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

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

См. Также stackoverflow.com/questions/1527856/…

rogerdpack 16.03.2015 23:04

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

Jonathan 23.07.2015 00:57

См. Также Как превратить строку в поток в Java?

Dangermouse 20.09.2016 15:40

Java 8: stackoverflow.com/a/47736566/1216775

akhil_mittal 11.08.2018 07:16
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
378
4
535 802
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

Ответ принят как подходящий

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

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

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

Я бы так и поступил. Мне это кажется самым простым.

Что касается правильности, я не верю, что она здесь существует. Все основано на вашем личном стиле.

Встраивает ли компилятор метод length ()?

Uri 13.10.2008 10:25

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

jjnguy 13.10.2008 10:28

@Uri, компилятор Java не выполняет оптимизацию. Для HotSpot JVM довольно скоро встроит его во время выполнения. Существуют и другие реализации JVM (то есть некоторые виртуальные машины J2ME, используемые в телефонах), которые не оптимизируют время выполнения.

ddimitrov 13.10.2008 10:50

он может встроить length (), то есть поднять метод, который вызывает несколько кадров, но это более эффективно для (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }

Dave Cheney 13.10.2008 12:04

Загромождение кода для увеличения производительности крошечный. Избегайте этого, пока не решите, что эта область кода критична по скорости.

slim 13.10.2008 12:13

Обычно я не оптимизирую свой код, если не жертвуют удобочитаемостью.

jjnguy 13.10.2008 18:18

Обратите внимание, что этот метод дает вам символы, а не кодовые точки, что означает, что вы можете получить суррогаты.

Gabe 24.03.2011 04:04

charAt - это не O (1) - это O (N) для суррогатов.

ikh 20.06.2014 14:22

@slim: Какого беспорядка вы советуете избегать - кеширование длины с помощью n? Или использовать цикл i вместо конструкции for-each?

LarsH 27.12.2016 20:06

@larsH в этом случае я говорил о n, но я также обычно кодировал конструкцию, которая также не использовала i.

slim 28.12.2016 12:09

@ikh charAt не O (1): Как так? Код для String.charAt(int) просто выполняет value[index]. Я думаю, вы путаете chatAt() с чем-то еще, что дает вам кодовые отметки.

antak 01.11.2018 09:45

что, если длина String больше, чем диапазон int?

Inder malviya 13.10.2019 09:05

@Indermalviya максимальная длина строки - Integer.MAX_VALUE

Siddhartha 07.06.2020 21:57

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

В javadoc говорится:

StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead.

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

ddimitrov 13.10.2008 10:56

ddimitrov: Я не понимаю, как указывать на то, что StringTokenizer не рекомендуется ВКЛЮЧАТЬ цитату из JavaDoc (java.sun.com/javase/6/docs/api/java/util/StringTokenizer.ht‌ мл), поскольку в нем говорится, что это вводит в заблуждение. Проголосовали за компенсацию.

Powerlord 13.10.2008 18:44

Спасибо, мистер Бемроуз ... Я полагаю, что процитированная цитата блока должна быть кристально ясной, из чего следует, вероятно, сделать вывод, что активные исправления ошибок не будут внесены в StringTokenizer.

Alan 14.10.2008 02:23

См. Учебники по 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.

Я начинаю чувствовать себя немного спамерским ... если есть такое слово :). Но это решение также имеет проблему, описанную здесь: Здесь та же проблема, что и здесь: stackoverflow.com/questions/196830/…

Emmanuel Oga 11.10.2014 11:49

Для этого есть несколько специальных классов:

import java.text.*;

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

Похоже на излишество для чего-то столь же простого, как итерация по неизменяемому массиву символов.

ddimitrov 13.10.2008 10:58

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

slim 13.10.2008 12:11

Если бы я использовал итератор, я бы тогда использовал цикл foreach.

jjnguy 13.10.2008 19:57

@jjnguy: foreach возможен только для java.lang.Iterable's

Bruno De Fraine 14.10.2008 12:00

Согласитесь с @ddimitrov - это перебор. Единственная причина использовать итератор - это воспользоваться преимуществом foreach, который немного легче «увидеть», чем цикл for. Если вы все равно собираетесь написать обычный цикл for, то с таким же успехом можно использовать charAt ()

Rob Gilliam 04.02.2010 11:39

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

ceving 18.06.2013 13:04

@ceving Не похоже, что итератор символов поможет вам с символами, отличными от BMP: oracle.com/us/technologies/java/supplementary-142654.html

Bruno De Fraine 27.06.2013 16:39

Два варианта

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

или же

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

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

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

Dennis 29.02.2012 21:43

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

Rhyous 15.05.2012 19:02

Есть еще мысли по этому поводу? Можем ли мы разумно ожидать, что оптимизация компилятора позаботится о том, чтобы избежать повторного вызова s.length (), или нет?

Matthias 14.08.2014 14:30

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

prasopes 09.10.2014 12:38

Также см. stackoverflow.com/questions/196830/…

Emmanuel Oga 11.10.2014 11:47

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

Isaac 25.12.2014 12:09

@DaveCheney, зачем вам определять 'n = s.length ()' вместо просто '(int i = 0; i <s.length (); i ++) {'?

Lasse 20.09.2015 13:45

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

Steve 23.11.2015 07:11

И, на мой взгляд, @Steve, на самом деле он менее читабелен, потому что (1) он нетрадиционный, поэтому он будет отвлекать людей, читающих ваш код (как это сделал Лассе и многие другие комментаторы), и (2) он отодвигает объявление от его использования .

DavidS 18.12.2015 21:53

Я не понимаю, почему первый может быть быстрее. Я думал, что foreach лучше всего оптимизирован для повышения производительности, не так ли?

Sergey Dirin 24.01.2018 09:21

toCharArray копирует содержимое String в новый массив, которого вы избегаете использовать charAt с обычным циклом for.

MattMerr47 09.08.2018 18:09

Также, что касается вызова s.length() в инициализаторе, это также преждевременная оптимизация из учебника, которая мешает читаемости ... не так ли?

Bruno Ely 09.02.2019 01:24

StringTokenizer совершенно не подходит для задачи разбиения строки на отдельные символы. С String#split() вы можете легко это сделать, используя регулярное выражение, которое ничего не соответствует, например:

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

Но StringTokenizer не использует регулярные выражения, и вы не можете указать строку-разделитель, которая будет соответствовать ничему между символами. является - один симпатичный маленький прием, который вы можете использовать для достижения того же самого: использовать саму строку в качестве строки-разделителя (делая каждый символ в ней разделителем) и заставить ее возвращать разделители:

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

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

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

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

Результат на моем Powerbook 2,6 ГГц (это Mac :-)) и JDK 1.5:

  • Тест 1: charAt + String -> 3138 мсек.
  • Тест 2: строка преобразована в массив -> 9568 мсек.
  • Тест 3: StringBuilder charAt -> 3536 мс
  • Тест 4: CharacterIterator и String -> 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");

Здесь та же проблема, описанная здесь: stackoverflow.com/questions/196830/…

Emmanuel Oga 11.10.2014 11:48

Обратите внимание, что большинство других описанных здесь методов не работают, если вы имеете дело с символами вне 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

Я не понимаю, как вы здесь используете что-либо, кроме Basic Multilingual Plane. curChar по-прежнему прав на 16 бит?

Prof. Falken 06.05.2011 16:21

Вы либо используете int для хранения всей кодовой точки, либо каждый char будет хранить только одну из двух суррогатных пар, которые определяют кодовую точку.

sk. 06.05.2011 23:15

Я думаю, мне нужно прочитать кодовые точки и суррогатные пары. Спасибо!

Prof. Falken 07.05.2011 00:59

+1, так как это, кажется, единственный ответ, который верен для символов Unicode вне BMP

Jason S 10.07.2014 20:08

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

Emmanuel Oga 12.10.2014 13:13

Важный момент, и его конкретно спрашивают по адресу: stackoverflow.com/questions/1527856/…

Ciro Santilli TRUMP BAN IS BAD 07.05.2015 18:46

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

sabujp 28.07.2019 04:48

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

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

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

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

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

Прорабатываем этот ответ и этот ответ.

Приведенные выше ответы указывают на проблему многих решений здесь, которые не повторяются по значению кодовой точки - у них будут проблемы с любым суррогатные символы. В документации java также описывается проблема здесь (см. «Представления символов Unicode»). Во всяком случае, вот код, который использует некоторые фактические суррогатные символы из дополнительного набора Unicode и преобразует их назад в String. Обратите внимание, что .toChars () возвращает массив символов: если вы имеете дело с суррогатами, у вас обязательно будет два символа. Этот код должен работать с символом Unicode Любые.

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

Этот пример кода поможет вам!

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

В 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, как указано в док:

Returns a stream of int zero-extending the char values from this sequence. Any char which maps to a surrogate code point is passed through uninterpreted. If the sequence is mutated while the stream is being read, the result is undefined.

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

Returns a stream of code point values from this sequence. Any surrogate pairs encountered in the sequence are combined as if by Character.toCodePoint and the result is passed to the stream. Any other code units, including ordinary BMP characters, unpaired surrogates, and undefined code units, are zero-extended to int values which are then passed to the stream.

Чем отличаются char и code point? Как упоминалось в статье это:

Unicode 3.1 added supplementary characters, bringing the total number of characters to more than the 2^16 = 65536 characters that can be distinguished by a single 16-bit char. Therefore, a char value no longer has a one-to-one mapping to the fundamental semantic unit in Unicode. JDK 5 was updated to support the larger set of character values. Instead of changing the definition of the char type, some of the new supplementary characters are represented by a surrogate pair of two char values. To reduce naming confusion, a code point will be used to refer to the number that represents a particular Unicode character, including supplementary ones.

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

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

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

Если вам нужна производительность, тогда вы должен проверить в своей среде. Другого пути нет.

Вот пример кода:

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

Так что обычно есть два способа перебрать строку в 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.

Другие вопросы по теме