Какой самый простой / лучший / самый правильный способ перебора символов строки в Java?


341

StringTokenizer? Преобразовать Stringк char[]и итерации над этим? Что-то другое?



3
Смотрите также stackoverflow.com/questions/1527856/...
rogerdpack

1
См. Также stackoverflow.com/questions/8894258/… Тесты показывают, что String.charAt () является самым быстрым для небольших строк, а использование отражения для чтения массива char напрямую для самых больших строк.
Джонатан


Ответы:


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
он может быть встроенным length (), то есть поднять метод, который вызывает несколько кадров, но это более эффективно сделать для (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
}

Первое, вероятно, быстрее, а второе, вероятно, более читабельно.


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

57
Я думал, что оптимизация компилятора позаботилась об этом за вас.
Rhyous

4
@Matthias Вы можете использовать дизассемблер класса Javap, чтобы увидеть, что повторные вызовы s.length () для выражения завершения цикла действительно исключены. Обратите внимание, что в коде OP опубликовано обращение к s.length () в выражении инициализации, поэтому семантика языка уже гарантирует, что он будет вызван только один раз.
прасопы

3
@prasopes Обратите внимание, что большинство оптимизаций Java происходят во время выполнения, а не в файлах классов. Даже если вы видели повторные вызовы length (), которые не указывают штраф за время выполнения, обязательно.
Исаак

2
@Lasse, предполагаемая причина в эффективности - ваша версия вызывает метод length () на каждой итерации, тогда как Дейв вызывает его один раз в инициализаторе. Тем не менее, вполне вероятно, что оптимизатор JIT («как раз вовремя») оптимизирует дополнительный вызов, так что, скорее всего, это только разница в удобочитаемости без реального выигрыша.
Стив

90

Обратите внимание, что большинство других методов, описанных здесь, ломаются, если вы имеете дело с символами вне BMP ( базовая многоязычная плоскость Unicode ), то есть кодовые точки, которые находятся за пределами диапазона 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
Джейсон С

Написал некоторый код, чтобы проиллюстрировать концепцию итерации по кодовым точкам
Эммануэль Ога

26

Я согласен, что StringTokenizer здесь перебор. На самом деле я опробовал предложения выше и не торопился.

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

Результат на моем Powerbook 2.6 ГГц (это mac :-)) и JDK 1.5:

  • Тест 1: charAt + String -> 3138 мсек
  • Тест 2: строка преобразуется в массив -> 9568 мс
  • Тест 3: символ StringBuilder -> 3536 мсек
  • Тест 4: CharacterIterator и строка -> 12151 мсек

Поскольку результаты значительно отличаются, самый простой способ также кажется самым быстрым. Интересно, что charAt () в StringBuilder кажется немного медленнее, чем в String.

Кстати, я предлагаю не использовать 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указанное в документе :

Возвращает поток значений int, расширяющих ноль, из этой последовательности. Любой символ, который отображается на суррогатную кодовую точку, пропускается через интерпретацию. Если последовательность видоизменяется во время чтения потока, результат не определен.

Метод codePoints()также возвращает IntStreamсогласно документу:

Возвращает поток значений кодовой точки из этой последовательности. Любые суррогатные пары, встречающиеся в последовательности, объединяются как бы с помощью Character.toCodePoint, и результат передается в поток. Любые другие единицы кода, включая обычные символы BMP, непарные суррогаты и неопределенные единицы кода, расширяются от нуля до значений int, которые затем передаются в поток.

Чем отличаются символ и код? Как уже упоминалось в этой статье:

В Unicode 3.1 добавлены дополнительные символы, в результате чего общее количество символов превышает 216 символов, которые можно различить одним 16-разрядным char. Поэтому charзначение больше не имеет однозначного сопоставления с основной семантической единицей в Юникоде. JDK 5 был обновлен для поддержки большего набора символьных значений. Вместо изменения определения charтипа, некоторые из новых дополнительных символов представлены суррогатной парой двух charзначений. Чтобы уменьшить путаницу имен, будет использоваться кодовая точка для обозначения номера, представляющего конкретный символ Unicode, включая дополнительные.

Наконец почему forEachOrderedи нет forEach?

Поведение forEachявляется явно недетерминированным, когда as 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
Выглядит как перебор для чего-то столь же простого, как итерация по неизменяемому массиву символов.
Ддимитров

1
Я не понимаю, почему это излишне. Итераторы - это самый простой способ сделать что-либо ... итеративный. StringCharacterIterator обязан в полной мере использовать неизменность.
тонкий

2
Согласитесь с @ddimitrov - это перебор. Единственная причина использовать итератор - использовать foreach, который немного легче "увидеть", чем цикл for. В любом случае, если вы собираетесь написать обычный цикл for, то можете использовать charAt ()
Роб Гиллиам,

3
Использование символьного итератора, вероятно, является единственным правильным способом перебора символов, поскольку Юникод требует больше места, чем charобеспечивает Java . Java charсодержит 16 бит и может содержать символы Unicode до U + FFFF, но Unicode определяет символы до U + 10FFFF. Использование 16 битов для кодирования Unicode приводит к кодированию символов переменной длины. Большинство ответов на этой странице предполагают, что кодировка Java является кодировкой постоянной длины, что неверно.
ceving

3
@ceving Похоже, что итератор персонажей не поможет вам с не-BMP-символами: oracle.com/us/technologies/java/supplementary-142654.html
Бруно Де Фрейн,

18

Если у вас есть Guava на вашем пути к классам, следующее является довольно удобочитаемой альтернативой. В Guava даже есть довольно разумная реализация List для этого случая, так что это не должно быть неэффективно.

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 + guava, поскольку вы не можете изменять переменные (например, целые числа и строки), определенные вне области действия forEach внутри forEach. Все, что находится внутри forEach, также не может генерировать проверенные исключения, так что иногда это тоже раздражает.
Sabujp

13

Если вам нужно перебрать точки кода String(см. Этот ответ ), более короткий / более читаемый способ - использовать CharSequence#codePointsметод, добавленный в Java 8:

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

или используя поток вместо цикла for:

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

Существует также, CharSequence#charsесли вы хотите, чтобы поток символов (хотя это IntStream, так как нет CharStream).


3

Я бы не стал использовать, так StringTokenizerкак это один из классов в JDK, который унаследован.

Javadoc говорит:

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


Строковый токенизатор - это совершенно правильный (и более эффективный) способ итерации по токенам (т. Е. Словам в предложении). Это определенно избыточно для итерации по символам. Я считаю ваш комментарий вводящим в заблуждение.
Ддимитров

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);

Однако я упоминаю только эти варианты с целью их отклонения. Оба метода разбивают исходную строку на односимвольные строки вместо символьных примитивов, и оба требуют больших накладных расходов в виде создания объекта и манипуляции со строками. Сравните это с вызовом charAt () в цикле for, который практически не требует дополнительных затрат.


0

Разрабатывая этот ответ и этот ответ .

Приведенные выше ответы указывают на проблему многих решений, которые здесь не повторяются по значению кодовой точки - у них возникнут проблемы с любыми суррогатными символами . Документы Java также описывают проблему здесь (см. «Представления символов Unicode»). Во всяком случае, вот некоторый код, который использует некоторые фактические суррогатные символы из дополнительного набора Unicode и преобразует их обратно в строку. Обратите внимание, что .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.