Java'da bir dizenin karakterleri arasında yineleme yapmanın en kolay / en iyi / en doğru yolu nedir?


341

StringTokenizer? Dönüştürmek Stringa char[]ve bunun üzerinde yineleme? Başka bir şey?




1
Ayrıca bkz. Stackoverflow.com/questions/8894258/… Göstergeler String.charAt () küçük dizeler için en hızlı ve char dizisini doğrudan okumak için yansıma kullanmak büyük dizeler için en hızlı olduğunu göstermektedir.
Jonathan


Yanıtlar:


363

Dizeyi yinelemek için for döngüsü kullanıyorum charAt()ve her karakteri incelemek için kullanıyorum. Dize bir dizi ile uygulandığından, charAt()yöntem sabit bir zaman işlemidir.

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

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

Ben de öyle yapardım. Benim için en kolay görünüyor.

Doğrulukla ilgili olarak, bunun var olduğuna inanmıyorum. Her şey kişisel tarzınıza dayanmaktadır.


3
Derleyici length () yöntemini satır içi yapar mı?
Uri

7
inline length () olabilir, bu da birkaç kareyi çağırmanın arkasındaki yöntemi kaldırır, ancak bunu yapmak için daha etkilidir (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Dave Cheney

32
Küçük bir performans artışı için kodunuzu karmaşık hale getirin . Bu kod alanının hız açısından kritik olduğuna karar verene kadar lütfen bundan kaçının.
ince

31
Bu tekniğin size kod noktaları değil karakterler verdiğini , yani vekilleri alabileceğinizi unutmayın.
Gabe

2
@ikh charAt O (1) değil : Bu nasıl? Kodu String.charAt(int)sadece yapıyor value[index]. Sanırım size chatAt()kod puanı veren başka bir şeyle karıştırıyorsunuz .
antak

209

İki seçenek

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

veya

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

Birincisi muhtemelen daha hızlı, ikincisi muhtemelen daha okunabilir.


26
artı s.length () öğesini başlatma ifadesine yerleştirmek için bir tane. Eğer kimse nedenini bilmiyorsa, bunun nedeni yalnızca bir kez değerlendirilir; burada, sonlandırma deyimine i <s.length () olarak yerleştirilmişse, s.length () her döngüde çağrılır.
Dennis

57
Derleyici optimizasyonunun bunu sizin için hallettiğini düşündüm.
Rhyous

4
@Matthias Döngü sonlandırma ifadesi için tekrarlanan s.length () çağrılarının gerçekten önlendiğini görmek için Javap sınıfı sökücüyü kullanabilirsiniz. Gönderilen OP kodunda s.length () çağrısının başlatma ifadesinde olduğunu unutmayın, bu nedenle dil semantiği zaten sadece bir kez çağrılacağını garanti eder.
14'te

3
@prasopes Çoğu java optimizasyonunun sınıf dosyalarında değil çalışma zamanında gerçekleştiğini unutmayın. Mutlaka bir çalışma zamanı cezası belirtmeyen length () çağrılarını tekrarlamış olsanız bile.
Isaac

2
@Lasse, varsayılan neden verimliliktir - sürümünüz her yinelemede length () yöntemini çağırırken Dave başlatıcıda bir kez çağırır. Bununla birlikte, JIT ("tam zamanında") optimize edicinin ekstra aramayı optimize edeceği büyük olasılıkla, gerçek kazanç olmadan sadece okunabilirlik farkı olması muhtemeldir.
Steve

90

BMP (Unicode Temel Çok Dilli Düzlem ) dışındaki karakterlerle , yani u0000-uFFFF aralığının dışındaki kod noktalarıyla ilgileniyorsanız, burada açıklanan diğer tekniklerin çoğunun parçalandığını unutmayın. Bunun dışındaki kod noktaları çoğunlukla ölü dillere atandığından, bu yalnızca nadiren olur. Ancak bunun dışında bazı yararlı karakterler vardır, örneğin matematiksel gösterim için kullanılan bazı kod noktaları ve bazıları Çince'de uygun adları kodlamak için kullanılır.

Bu durumda kodunuz:

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)Yöntem Java 5+ gerektirir.

Kaynak: http://mindprod.com/jgloss/codepoint.html


1
Burada Temel Çok Dilli Düzlem'den başka hiçbir şeyi nasıl kullandığınızı anlamıyorum. curChar hala 16 bit doğru mu?
Prof. Falken sözleşmesi

2
Tüm kod noktasını saklamak için bir int kullanırsınız, aksi takdirde her karakter kod noktasını tanımlayan iki yedek çiftten yalnızca birini saklar.
sk.

1
Kod noktalarını ve vekil çiftleri okumam gerektiğini düşünüyorum. Teşekkürler!
Prof. Falken sözleşmesi

6
Bu BMP dışındaki Unicode karakterleri için doğru tek cevap gibi görünüyor
Jason S

Kod noktaları üzerinde yineleme kavramını göstermek için bazı kodlar yazdı (karakterlerin aksine): gist.github.com/EmmanuelOga/…
Emmanuel Oga

26

StringTokenizer'in burada aşırı dolu olduğunu kabul ediyorum. Aslında yukarıdaki önerileri denedim ve zaman aldım.

Testim oldukça basitti: yaklaşık bir milyon karakterden oluşan bir StringBuilder oluşturun, bir String'e dönüştürün ve charAt () ile / bir char dizisine / CharacterIterator ile bin kez dönüştürdükten sonra her birini çaprazlayın (tabii ki dize üzerinde bir şey yapın, böylece derleyici tüm döngüyü optimize edemez :-)).

2.6 GHz Powerbook'um (bu bir mac :-)) ve JDK 1.5'teki sonuç:

  • Test 1: charAt + String -> 3138msn
  • Test 2: Diziye dönüştürülen dize -> 9568msn
  • Test 3: StringBuilder charAt -> 3536 msn
  • Test 4: CharacterIterator ve String -> 12151msn

Sonuçlar önemli ölçüde farklı olduğundan, en basit yol da en hızlı yol gibi görünmektedir. İlginç bir şekilde, bir StringBuilder'ın charAt () yöntemi, String'inkinden biraz daha yavaş görünüyor.

BTW Ben '\ uFFFF' karakterini kötüye kullanımı "yineleme sonu" olarak gerçekten korkunç bir kesmek olarak kabul CharacterIterator kullanmanızı öneririz. Büyük projelerde her zaman iki farklı amaç için aynı tür kesmek kullanan iki adam vardır ve kod gerçekten gizemli bir şekilde çöküyor.

İşte testlerden biri:

    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
Bu aynı sorunu burada
Emmanuel Oga

22

In Java 8 bunu olarak çözebilir:

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

Yöntem karakter () bir döner IntStreambelirtildiği gibi doküman :

Bu diziden char değerlerini sıfır genişleten bir int akışı döndürür. Bir vekil kod noktasına eşlenen tüm karakterler yorumlanmadan geçirilir. Akış okunurken dizi mutasyona uğratılırsa, sonuç tanımsız olur.

Yöntem codePoints()ayrıca IntStreamdoc başına bir döndürür :

Bu diziden bir kod noktası değerleri akışı döndürür. Dizide karşılaşılan yedek çiftler, Character.toCodePoint tarafından birleştirilir ve sonuç akışa geçirilir. Sıradan BMP karakterleri, eşleştirilmemiş vekiller ve tanımlanmamış kod birimleri de dahil olmak üzere diğer kod birimleri, daha sonra akışa iletilen int değerlerine sıfır olarak genişletilir.

Karakter ve kod noktası arasındaki fark nedir? Bu makalede belirtildiği gibi :

Unicode 3.1 ek karakter ekleyerek toplam karakter sayısını 216 karakterden fazlasını tek bir 16 bit ile ayırt edebilecek şekilde getirdi char. Bu nedenle, bir chardeğerin artık Unicode'daki temel semantik birim ile bire bir eşlemesi yoktur. JDK 5, daha büyük karakter değerleri kümesini destekleyecek şekilde güncellendi. Türün tanımını değiştirmek yerine char, yeni tamamlayıcı karakterlerin bazıları iki chardeğerden oluşan bir vekil çiftle temsil edilir . Adlandırma karışıklığını azaltmak için, ek olanlar da dahil olmak üzere belirli bir Unicode karakterini temsil eden sayıyı belirtmek için bir kod noktası kullanılacaktır.

Sonunda neden forEachOrderedolmasın forEach?

Öğesinin davranışı forEachaçıkça belirsizdir; burada forEachOrderedakış, tanımlı bir karşılaşma sırasına sahipse akışın karşılaşma sırasında bu akışın her öğesi için bir eylem gerçekleştirir . Bu yüzden forEachsiparişin korunacağını garanti etmez. Ayrıca daha fazla bilgi için bu soruyu kontrol edin .

Bir karakter, bir kod noktası, bir glif ve bir grafik arasındaki fark için bu soruyu kontrol edin .


21

Bunun için bazı özel sınıflar var:

import java.text.*;

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

7
Değişmez karakter dizisi üzerinde yineleme yapmak kadar basit bir şey için aşırıya kaçmış gibi görünüyor.
ddimitrov

1
Bunun neden aşırı olduğunu bilmiyorum. Yineleyiciler, her şeyi yapmanın en java-ish yoludur ... yinelemeli. StringCharacterIterator değişmezlikten tam olarak yararlanmak zorundadır.
ince

2
@Ddimitrov ile aynı fikirde - bu aşırıya kaçma. Bir yineleyici kullanmanın tek nedeni foreach'ten faydalanmak olacaktır, ki bu bir for döngüsünden "görmek" biraz daha kolaydır. Yine de bir döngü için konvansiyonel yazacaksanız, charAt ()
Rob Gilliam

3
Karakter yineleyiciyi kullanmak muhtemelen karakterler üzerinde yineleme yapmanın tek doğru yoludur, çünkü Unicode bir Java'dan daha fazla alan gerektirir char. Bir Java char16 bit içerir ve Unicode karakterleri U + FFFF kadar tutabilir ancak Unicode U + 10FFFF değerine kadar olan karakterleri belirtir. Unicode'u kodlamak için 16 bit kullanmak değişken uzunluklu bir karakter kodlamasıyla sonuçlanır. Bu sayfadaki cevapların çoğu, Java kodlamasının yanlış uzunluktaki sabit bir kodlama olduğunu varsayar.
ceving

3
@ceving Bir karakter yineleyicinin BMP dışı karakterler için size yardımcı
olacağı görülmüyor

18

Sınıf yolunuzda Guava varsa , aşağıdakiler oldukça okunabilir bir alternatiftir. Guava'nın bu durum için oldukça makul bir özel Liste uygulaması bile var, bu yüzden bu verimsiz olmamalıdır.

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

GÜNCELLEME: @Alex'in belirttiği gibi, Java 8 ile de CharSequence#charskullanılacak. Tür bile IntStream olduğundan, aşağıdaki gibi karakterlerle eşleştirilebilir:

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

Karmaşık bir şey yapmanız gerekiyorsa forEach içindeki forEach kapsamı dışında tanımlanan değişkenleri (örn. Tamsayılar ve Dizeler) değiştiremediğiniz için for loop + guava ile gidin. ForEach içinde ne varsa, kontrol edilen istisnaları da atamaz, bu yüzden bu bazen can sıkıcıdır.
sabujp

13

Bir a'nın kod noktalarını yinelemeniz gerekiyorsa String(bu cevaba bakınız ) daha kısa / daha okunabilir bir yol CharSequence#codePointsJava 8'de eklenen yöntemi kullanmaktır :

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

veya akışı for for döngüsü yerine doğrudan kullanma:

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

Ayrıca CharSequence#charskarakterlerin bir akışını istiyorsanız (o olmasına rağmen IntStream, çünkü bir yok CharStream).


3

StringTokenizerJDK'daki eski sınıflardan biri olduğu için kullanmazdım .

Javadoc diyor ki:

StringTokenizerkullanımı yeni kodda kullanılmamasına rağmen uyumluluk nedeniyle saklanan eski bir sınıftır. Bu işlevselliği arayan herkesin bunun yerine split yöntemini Stringveya java.util.regexpaketini kullanması önerilir .


String tokenizer, tokenleri yinelemek için mükemmel bir şekilde geçerli (ve daha verimli) bir yoldur (yani, bir cümledeki kelimeler). Yorumunuzu yanıltıcı olarak reddediyorum.
ddimitrov

3
ddimitrov: StringTokenizer'ın JavaDoc'dan ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ) bir teklif dahil olduğu gibi nasıl önerilmediğini belirtmiyorum. yanıltıcı. Ofset için oylandı.
Powerlord

1
Teşekkürler Bay Bemrose ... Alıntılanan blok alıntısının kristal berraklığında olması gerektiğini düşünüyorum, burada muhtemelen aktif hata düzeltmelerinin StringTokenizer'e taahhüt edilmeyeceğini çıkarması gerekir.
Alan

2

Performansa ihtiyacınız varsa , ortamınızı test etmeniz gerekir . Başka yol yok.

İşte örnek kod:

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

On Java çevrimiçi alıyorum:

1 10349420
2 526130
3 484200
0

Android x86 API 17'de şunu elde ederim:

1 9122107
2 13486911
3 12700778
0

0

Bkz . Java Öğreticileri: Dizeler .

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

Uzunluğu koyun int lenve fordöngüyü kullanın .


1
Biraz spamcı hissetmeye başlıyorum ... eğer böyle bir kelime varsa :). Ancak bu çözümün de burada özetlenen problemi var: Bu, burada özetlenen aynı soruna sahiptir: stackoverflow.com/questions/196830/…
Emmanuel Oga

0

StringTokenizer, bir dizeyi kendi karakterlerine bölme görevi için tamamen uygun değildir. Bununla String#split()hiçbir şeyle eşleşen bir normal ifade kullanarak bunu kolayca yapabilirsiniz, örneğin:

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

Ancak StringTokenizer normal ifadeleri kullanmaz ve karakterler arasında hiçbir şeyle eşleşmeyecek belirtebileceğiniz sınırlayıcı bir dize yoktur. Orada ise bu ayraçları dönmek Ayırıcı dize olarak dize kendisi kullanmak (buna her karakteri bir ayırıcı yapma) ve vardır: biri sevimli küçük aynı şeyi gerçekleştirmek için kullanabileceğiniz kesmek:

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

Ancak, bu seçeneklerden sadece onları reddetmek amacıyla bahsediyorum. Her iki teknik de orijinal dizgiyi karakter ilkelleri yerine tek karakterli dizelere böler ve her ikisi de nesne oluşturma ve dizgi manipülasyonu şeklinde büyük miktarda ek yük içerir. Bunu neredeyse hiç ek yükü olmayan bir for döngüsünde charAt () öğesini çağırarak karşılaştırın.


0

Bu cevap ve bu cevap üzerinde duruluyor .

Yukarıdaki yanıtlar, kod noktası değerine göre yinelenmeyen birçok çözümün sorununa işaret ediyor - herhangi bir vekil grafikle sorun yaşayacaklardı . Java dokümanları da sorunu burada özetliyor (bkz. "Unicode Karakter Gösterimleri"). Her neyse, ek Unicode kümesinden bazı gerçek vekil karakterleri kullanan ve bunları tekrar bir String'e dönüştüren bazı kodlar . .ToChars () işlevinin bir dizi karakter döndürdüğünü unutmayın: Taşıyıcılarla uğraşıyorsanız, mutlaka iki karaktere sahip olursunuz. Bu kod herhangi bir Unicode karakteri için çalışmalıdır .

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

0

Bu Örnek Kod size yardımcı olacaktır!

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

Yani tipik olarak, bu konudaki birden fazla kişi tarafından zaten cevaplanmış olan java dizesini yinelemenin iki yolu var, sadece benim versiyonumu ekliyoruz.

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

Performans söz konusu ise, o zaman ilkini sabit zamanda kullanmanızı tavsiye ederim, eğer ikincisi ile devam etmiyorsa, Java'daki string sınıflarıyla değişmezliği göz önünde bulundurarak işinizi kolaylaştırır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.