Який найпростіший / найкращий / найправильніший спосіб перебрати символи рядка на Java?


341

StringTokenizer? Перетворити Stringна а char[]і повторити це? Щось ще?




1
Дивіться також stackoverflow.com/questions/8894258 / ... тести показують String.charAt () є найшвидшим для коротких рядків, а з допомогою відображення , щоб прочитати масив символів безпосередньо є найшвидшим для великих рядків.
Джонатан


Відповіді:


363

Я використовую цикл for для повторення рядка і використовую charAt()для отримання кожного символу для його вивчення. Оскільки String реалізований з масивом, charAt()метод є операцією постійного часу.

String s = "...stuff...";

for (int i = 0; i < s.length(); i++){
    char c = s.charAt(i);        
    //Process char
}

Це я і зробив. Мені це здається найлегшим.

Що стосується коректності, я не вірю, що тут існує. Все базується на вашому особистому стилі.


3
Чи вкладає компілятор метод length ()?
Урі

7
це може бути вбудована довжина (), тобто піднімає метод за цим викликом декількох кадрів, але його ефективніше зробити для цього (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Дейв Чейні

32
Захаращуючи свій код для невеликого підвищення продуктивності. Будь ласка, уникайте цього, поки не вирішите, що ця область коду є критичною.
стрункий

31
Зауважте, що ця методика надає символи , а не кодові очки , тобто ви можете отримати сурогати.
Гейб

2
@ikh charAt не є O (1) : Як це так? Код для String.charAt(int)цього просто робиться value[index]. Я думаю, ви плутаєте chatAt()щось із іншим, що дає вам кодові бали.
антак

209

Два варіанти

for(int i = 0, n = s.length() ; i < n ; i++) { 
    char c = s.charAt(i); 
}

або

for(char c : s.toCharArray()) {
    // process c
}

Перший, ймовірно, швидший, а потім 2-й, мабуть, читабельніший.


26
плюс один для розміщення s.length () у виразі ініціалізації. Якщо хтось не знає чому, це тому, що це оцінюється лише один раз, де, якби він був розміщений у операторі завершення як i <s.length (), тоді s.length () буде називатися кожного разу, коли воно циклічно.
Денніс

57
Я думав, що оптимізація компілятора подбала про це для вас.
Rhyous

4
@Matthias Ви можете скористатися розбиральником класу Javap, щоб побачити, що дійсно уникається повторних викликів s.length () для вираження завершення циклу. Зауважте, що в коді OP, розміщений заклик до s.length (), є вираз ініціалізації, тому мовна семантика вже гарантує, що він буде викликаний лише один раз.
prasopes

3
@prasopes Зауважте, що більшість Java-оптимізацій відбувається під час виконання, а не у файлах класу. Навіть якщо ви бачили неодноразові дзвінки на тривалість (), які не вказують на строк виконання, обов'язково.
Ісаак

2
@Lasse, можлива причина в ефективності - ваша версія викликає метод length () під час кожної ітерації, тоді як Дейв викликає його один раз у ініціалізаторі. При цьому, велика ймовірність, що оптимізатор JIT ("точно вчасно") оптимізує додатковий виклик, тому, ймовірно, це лише різниця в читанні без реального виграшу.
Стів

90

Зверніть увагу, що більшість інших описаних тут методів розбиваються, якщо ви маєте справу з символами поза BMP (Unicode Basic Multilingual Plane ), тобто кодовими точками які знаходяться за межами діапазону u0000-uFFFF. Це трапляється лише рідко, оскільки точки коду поза цим здебільшого призначаються мертвим мовам. Але є деякі корисні символи поза цим, наприклад, деякі кодові точки, що використовуються для математичного позначення, а деякі використовуються для кодування власних імен китайською мовою.

У такому випадку ваш код буде:

String str = "....";
int offset = 0, strLen = str.length();
while (offset < strLen) {
  int curChar = str.codePointAt(offset);
  offset += Character.charCount(curChar);
  // do something with curChar
}

Character.charCount(int)Метод вимагає Java 5+.

Джерело: http://mindprod.com/jgloss/codepoint.html


1
Я не розумію, як ви тут використовуєте що-небудь, крім базової багатомовної площини. curChar все ще 16 біт правий?
Контракт професора Фолкена було порушено

2
Ви або використовуєте int для зберігання всієї кодової точки, інакше кожен знак буде зберігати лише одну з двох сурогатних пар, які визначають кодову точку.
ск.

1
Я думаю, що мені потрібно прочитати кодові пункти та сурогатні пари. Дякую!
Контракт професора Фолкена було порушено

6
+1, оскільки, здається, це єдина відповідь, яка є правильною для символів Unicode за межами BMP
Jason S

Написав код, щоб проілюструвати концепцію повторення кодових точок (на відміну від символів): gist.github.com/EmmanuelOga/…
Еммануель Ога

26

Я згоден, що StringTokenizer тут непосильний. Насправді я випробував запропоновані вище пропозиції і знайшов час.

Мій тест був досить простим: створити StringBuilder з близько мільйона символів, перетворити його в String і пройти кожен з них за допомогою charAt () / після перетворення в масив char / тисячу разів (звичайно переконуючись, що зробіть щось у рядку, щоб компілятор не зміг оптимізувати весь цикл :-)).

Результат у моїй книжці 2,6 ГГц (це мак :-)) та JDK 1,5:

  • Тест 1: charAt + String -> 3138msec
  • Тест 2: Рядок, перетворений у масив -> 9568msec
  • Тест 3: charAt StringBuilder -> 3536msec
  • Тест 4: CharacterIterator and String -> 12151msec

Оскільки результати значно відрізняються, найпростіший спосіб також видається найшвидшим. Цікаво, що charAt () StringBuilder здається трохи повільнішим, ніж у StringBuilder.

BTW Я пропоную не використовувати CharacterIterator, оскільки вважаю його зловживання символом "\ uFFFF" "кінцем ітерації" справді жахливим злом. У великих проектах завжди є два хлопці, які використовують один і той же вид зламу для двох різних цілей, і код виходить дійсно загадково.

Ось один із тестів:

    int count = 1000;
    ...

    System.out.println("Test 1: charAt + String");
    long t = System.currentTimeMillis();
    int sum=0;
    for (int i=0; i<count; i++) {
        int len = str.length();
        for (int j=0; j<len; j++) {
            if (str.charAt(j) == 'b')
                sum = sum + 1;
        }
    }
    t = System.currentTimeMillis()-t;
    System.out.println("result: "+ sum + " after " + t + "msec");

1
У цій же проблемі описано тут: stackoverflow.com/questions/196830/…
Еммануель Ога

22

У Java 8 ми можемо вирішити це як:

String str = "xyz";
str.chars().forEachOrdered(i -> System.out.print((char)i));
str.codePoints().forEachOrdered(i -> System.out.print((char)i));

Метод chars () повертає, IntStreamяк зазначено в doc :

Повертає потік int, що розширює значення char із цієї послідовності. Будь-яка таблиця, яка відображається в сурогатній кодовій точці, передається через неінтерпретоване. Якщо послідовність мутується під час читання потоку, результат не визначений.

Метод codePoints()також повертає IntStreamяк на документ:

Повертає потік значень точок коду з цієї послідовності. Будь-які сурогатні пари, що зустрічаються в послідовності, поєднуються як би символом.toCodePoint і результат передається в потік. Будь-які інші одиниці коду, включаючи звичайні символи BMP, парні сурогати та неозначені кодові одиниці, розширюються на нуль до значень int, які потім передаються в потік.

Чим відрізняються char та code? Як згадується в цій статті:

Unicode 3.1 додав додаткові символи, що збільшить загальну кількість символів більше ніж 216 символів, які можна розрізнити одним 16-бітовим char. Отже, charзначення більше не має відображення один на один до основної смислової одиниці в Unicode. JDK 5 було оновлено для підтримки більшого набору знакових значень. Замість зміни визначення charтипу деякі нові додаткові символи представлені сурогатною парою з двох charзначень. Щоб зменшити плутанину імен, буде використана кодова точка для позначення числа, що представляє певний символ Unicode, включаючи додаткові.

Нарешті чому forEachOrderedі ні forEach?

Поведінка forEachявно недетерміновано там, де forEachOrderedвиконується дія для кожного елемента цього потоку, у порядку зустрічі потоку, якщо потік має визначений порядок зустрічі. Тож forEachне гарантує, що порядок буде дотримано. Також перевірте це питання для отримання додаткової інформації.

Для різниці між персонажем, кодовою точкою, гліфом та графемою перевірте це запитання .


21

Для цього є кілька виділених занять:

import java.text.*;

final CharacterIterator it = new StringCharacterIterator(s);
for(char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
   // process c
   ...
}

7
Виглядає як надлишок для чогось такого простого, як ітерація над незмінним масивом char.
ddimitrov

1
Я не бачу, чому це надмірно. Ітератори - це найважливіший спосіб зробити що-небудь ... ітеративний. StringCharacterIterator зобов'язаний повністю скористатися незмінністю.
тонкий

2
Погодьтеся з @ddimitrov - це надмірність. Єдиною причиною використання ітератора було б скористатись функцією foreach, яку "легше" побачити, ніж для циклу. Якщо ви все-таки будете писати звичайне для циклу, тоді ви також можете скористатися charAt ()
Роб Гілліам

3
Використання ітератора символів є, мабуть, єдиним правильним способом перебору символів, оскільки Unicode вимагає більше місця, ніж charнадає Java . Java charмістить 16 біт і може містити символи Unicode до U + FFFF, але Unicode вказує символи до U + 10FFFF. Використання 16 біт для кодування Unicode призводить до кодування символів змінної довжини. Більшість відповідей на цій сторінці припускають, що кодування Java - це кодування постійної довжини, що неправильно.
закінчення

3
@ceving Не здається, що ітератор символів збирається допомогти вам із символами, що не належать до BMP: oracle.com/us/technologies/java/supplementary-142654.html
Бруно Де

18

Якщо у вас є гуава на вашому занятті, наступне - це досить читана альтернатива. У Guava навіть є досить розумна реалізація спеціального списку для цього випадку, тому це не повинно бути неефективним.

for(char c : Lists.charactersOf(yourString)) {
    // Do whatever you want     
}

ОНОВЛЕННЯ: Як зазначав @Alex, з Java 8 також CharSequence#charsможна використовувати. Навіть тип - це IntStream, тому його можна відобразити на символи типу:

yourString.chars()
        .mapToObj(c -> Character.valueOf((char) c))
        .forEach(c -> System.out.println(c)); // Or whatever you want

Якщо вам потрібно зробити що-небудь складне, тоді перейдіть до циклу for loop + guava, оскільки ви не можете мутувати змінні (наприклад, Integers і Strings), визначені за межами області forEach всередині forEach. Все, що знаходиться всередині forEach, також не може кидати перевірені винятки, тому іноді це також дратує.
sabujp

13

Якщо вам потрібно перебрати точки коду String(див. Цю відповідь ), більш коротким / читабельним способом є використання CharSequence#codePointsметоду, доданого в Java 8:

for(int c : string.codePoints().toArray()){
    ...
}

або використовуючи потік безпосередньо замість циклу:

string.codePoints().forEach(c -> ...);

Існує також, CharSequence#charsякщо ви хочете потік символів (хоча це і є IntStream, оскільки немає CharStream).


3

Я б не використовував, StringTokenizerоскільки це один із класів у JDK, який є спадщиною.

Явадок каже:

StringTokenizerце старий клас, який зберігається з міркувань сумісності, хоча його використання не перешкоджає новому коду. Рекомендується, щоб кожен, хто шукає цю функціональність, використовував метод розділення Stringабо java.util.regexпакет замість цього.


Струнний токенізатор - це цілком правильний (і більш ефективний) спосіб ітерації над лексемами (тобто словами в реченні). Це, безумовно, надмірне значення для повторення символів. Я вважаю ваш коментар оманливим.
ddimitrov

3
ddimitrov: Я не слідкую за тим, як вказувати на те, що StringTokenizer не рекомендується Включати цитату з JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ), оскільки вона заявляє, що така є введення в оману. Запропоновано компенсувати.
Powerlord

1
Дякую, містер Бемроуз ... я вважаю, що цитований блок цитати повинен був бути кришталево чистим, де, мабуть, слід зробити висновок, що активні виправлення помилок не будуть зараховані до StringTokenizer.
Алан

2

Якщо вам потрібна продуктивність, то ви повинні перевірити своє оточення. Іншого шляху немає.

Ось приклад коду:

int tmp = 0;
String s = new String(new byte[64*1024]);
{
    long st = System.nanoTime();
    for(int i = 0, n = s.length(); i < n; i++) {
        tmp += s.charAt(i);
    }
    st = System.nanoTime() - st;
    System.out.println("1 " + st);
}

{
    long st = System.nanoTime();
    char[] ch = s.toCharArray();
    for(int i = 0, n = ch.length; i < n; i++) {
        tmp += ch[i];
    }
    st = System.nanoTime() - st;
    System.out.println("2 " + st);
}
{
    long st = System.nanoTime();
    for(char c : s.toCharArray()) {
        tmp += c;
    }
    st = System.nanoTime() - st;
    System.out.println("3 " + st);
}
System.out.println("" + tmp);

На Java онлайн я отримую:

1 10349420
2 526130
3 484200
0

На Android x86 API 17 я отримую:

1 9122107
2 13486911
3 12700778
0

0

Див . Підручники Java: Струни .

public class StringDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";
        int len = palindrome.length();
        char[] tempCharArray = new char[len];
        char[] charArray = new char[len];

        // put original string in an array of chars
        for (int i = 0; i < len; i++) {
            tempCharArray[i] = palindrome.charAt(i);
        } 

        // reverse array of chars
        for (int j = 0; j < len; j++) {
            charArray[j] = tempCharArray[len - 1 - j];
        }

        String reversePalindrome =  new String(charArray);
        System.out.println(reversePalindrome);
    }
}

Покладіть довжину int lenі використовуйте forпетлю.


1
Я починаю трохи спаммірувати ... якщо таке слово є :). Але і в цьому рішенні є проблема, окреслена тут: У цій же проблемі описана тут: stackoverflow.com/questions/196830/…
Еммануель Ога,

0

StringTokenizer абсолютно не підходить до завдання розбити рядок на окремі символи. З цим String#split()можна легко зробити, використовуючи регулярний вираз, який нічого не відповідає, наприклад:

String[] theChars = str.split("|");

Але StringTokenizer не використовує регулярні вирази, і немає жодного рядка відмежувача, який можна вказати, який би нічого не відповідав символам. Там є один милий маленький хак ви можете використовувати , щоб зробити те ж саме: використовувати саму рядок в якості рядка роздільників (роблячи кожен символ в ньому роздільників), вони повинні повернути роздільники:

StringTokenizer st = new StringTokenizer(str, str, true);

Однак я згадую лише ці варіанти з метою їх звільнення. Обидві методи розбивають початковий рядок на односимвольні рядки замість char примітивів, і обидва передбачають великі накладні витрати у вигляді створення об'єкта та маніпуляції з рядками. Порівняйте це з викликом charAt () у циклі for, який практично не має накладних витрат.


0

Розвиваючи цю відповідь і цю відповідь .

Наведені вище відповіді вказують на проблему багатьох тут рішень, які не повторюються за значенням кодової точки - у них виникнуть проблеми з будь-якими сурогатними знаками . Документи Java також окреслюють проблему тут (див. "Представлення символів Unicode"). У будь-якому випадку, ось якийсь код, який використовує деякі фактичні сурогатні символи з додаткового набору Unicode і перетворює їх назад у String. Зауважте, що .toChars () повертає масив символів: якщо ви маєте справу з сурогатами, у вас обов'язково буде два символи. Цей код повинен працювати для будь-якого символу Unicode.

    String supplementary = "Some Supplementary: 𠜎𠜱𠝹𠱓";
    supplementary.codePoints().forEach(cp -> 
            System.out.print(new String(Character.toChars(cp))));

0

Цей приклад коду допоможе вам вийти!

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class Solution {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        map.put("a", 10);
        map.put("b", 30);
        map.put("c", 50);
        map.put("d", 40);
        map.put("e", 20);
        System.out.println(map);

        Map sortedMap = sortByValue(map);
        System.out.println(sortedMap);
    }

    public static Map sortByValue(Map unsortedMap) {
        Map sortedMap = new TreeMap(new ValueComparator(unsortedMap));
        sortedMap.putAll(unsortedMap);
        return sortedMap;
    }

}

class ValueComparator implements Comparator {
    Map map;

    public ValueComparator(Map map) {
        this.map = map;
    }

    public int compare(Object keyA, Object keyB) {
        Comparable valueA = (Comparable) map.get(keyA);
        Comparable valueB = (Comparable) map.get(keyB);
        return valueB.compareTo(valueA);
    }
}

0

Тому, як правило, є два способи ітерації через рядок у Java, на що вже відповіли декілька людей тут, у цій темі, просто додавши мою версію. Спочатку використовується

String s = sc.next() // assuming scanner class is defined above
for(int i=0; i<s.length; i++){
     s.charAt(i)   // This being the first way and is a constant time operation will hardly add any overhead
  }

char[] str = new char[10];
str = s.toCharArray() // this is another way of doing so and it takes O(n) amount of time for copying contents from your string class to character array

Якщо нагромаджено продуктивність, то я рекомендую використовувати перший у постійному часі, якщо він не йде, тоді другий полегшує вашу роботу, враховуючи незмінність класів струн у java.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.