Cách dễ nhất / tốt nhất / đúng nhất để lặp qua các ký tự của một chuỗi trong Java là gì?


340

StringTokenizer? Chuyển đổi Stringthành a char[]và lặp đi lặp lại điều đó? Thứ gì khác?




1
Xem thêm stackoverflow.com/questions/8894258/ Điểm chuẩn cho thấy String.charAt () là nhanh nhất đối với các chuỗi nhỏ và sử dụng phản xạ để đọc trực tiếp mảng char là nhanh nhất đối với các chuỗi lớn.
Jonathan


Câu trả lời:


362

Tôi sử dụng một vòng lặp for để lặp lại chuỗi và sử dụng charAt()để lấy từng ký tự để kiểm tra nó. Vì Chuỗi được triển khai với một mảng, nên charAt()phương thức là một hoạt động thời gian không đổi.

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

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

Đó là những gì tôi sẽ làm. Nó có vẻ dễ nhất với tôi.

Theo như sự đúng đắn, tôi không tin rằng có tồn tại ở đây. Tất cả đều dựa trên phong cách cá nhân của bạn.


3
Trình biên dịch nội tuyến phương thức length ()?
Uri

7
nó có thể có độ dài nội tuyến (), đó là phương thức di chuyển phía sau gọi lên một vài khung hình, nhưng hiệu quả hơn để làm điều này cho (int i = 0, n = s.length (); i <n; i ++) {char c = s.charAt (i); }
Dave Cheney

32
Làm lộn xộn mã của bạn để đạt được hiệu suất nhỏ . Vui lòng tránh điều này cho đến khi bạn quyết định khu vực mã này là rất quan trọng.
mỏng

31
Lưu ý rằng kỹ thuật này cung cấp cho bạn các ký tự , không phải điểm mã , có nghĩa là bạn có thể có người thay thế.
Gabe

2
@ikh charAt không phải là O (1) : Làm sao vậy? Mã cho String.charAt(int)chỉ đơn giản là làm value[index]. Tôi nghĩ rằng bạn đang nhầm lẫn chatAt()với một cái gì đó cung cấp cho bạn điểm mã.
antak

208

Hai lựa chọn

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

hoặc là

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

Đầu tiên có lẽ là nhanh hơn, sau đó thứ 2 có thể dễ đọc hơn.


26
cộng với một để đặt s.length () trong biểu thức khởi tạo. Nếu bất cứ ai không biết tại sao, thì đó chỉ là do nó được đánh giá một lần nếu nó được đặt trong câu lệnh chấm dứt là i <s.length (), thì s.length () sẽ được gọi mỗi lần lặp.
Dennis

57
Tôi nghĩ tối ưu hóa trình biên dịch đã chăm sóc điều đó cho bạn.
Rhyous

4
@Matthias Bạn có thể sử dụng trình phân tách lớp Javap để thấy rằng các lệnh gọi lặp lại đến s.length () trong biểu thức chấm dứt vòng lặp thực sự bị tránh. Lưu ý rằng trong mã OP đã đăng lệnh gọi s.length () nằm trong biểu thức khởi tạo, vì vậy ngữ nghĩa ngôn ngữ đã đảm bảo rằng nó sẽ chỉ được gọi một lần.
prasopes

3
@prasopes Lưu ý rằng hầu hết các tối ưu hóa java xảy ra trong thời gian chạy, KHÔNG phải trong các tệp lớp. Ngay cả khi bạn thấy các cuộc gọi lặp lại theo chiều dài () không chỉ ra hình phạt thời gian chạy, nhất thiết phải có.
Isaac

2
@Lasse, lý do giả định là vì hiệu quả - phiên bản của bạn gọi phương thức length () trên mỗi lần lặp, trong khi Dave gọi nó một lần trong trình khởi tạo. Điều đó nói rằng, rất có khả năng trình tối ưu hóa JIT ("chỉ trong thời gian") sẽ tối ưu hóa cuộc gọi thêm, do đó, nó chỉ có thể là một sự khác biệt dễ đọc cho không có lợi ích thực sự.
Steve

90

Lưu ý hầu hết các kỹ thuật khác được mô tả ở đây bị hỏng nếu bạn đang xử lý các ký tự bên ngoài BMP ( Mặt phẳng đa ngôn ngữ Unicode cơ bản ), tức là các điểm mã nằm ngoài phạm vi u0000-uFFFF. Điều này sẽ chỉ xảy ra hiếm khi, vì các điểm mã bên ngoài này hầu hết được gán cho các ngôn ngữ chết. Nhưng có một số ký tự hữu ích bên ngoài này, ví dụ một số điểm mã được sử dụng cho ký hiệu toán học và một số được sử dụng để mã hóa tên riêng trong tiếng Trung.

Trong trường hợp đó, mã của bạn sẽ là:

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

Các Character.charCount(int)phương pháp đòi hỏi Java 5 +.

Nguồn: http://mindprod.com/jgloss/codepoint.html


1
Tôi không hiểu làm thế nào bạn sử dụng bất cứ thứ gì ngoại trừ Mặt phẳng đa ngôn ngữ cơ bản ở đây. curChar vẫn còn 16 bit?
Hợp đồng của giáo sư Falken vi phạm

2
Bạn có thể sử dụng một int để lưu trữ toàn bộ điểm mã, nếu không, mỗi char sẽ chỉ lưu trữ một trong hai cặp thay thế xác định điểm mã.
sk.

1
Tôi nghĩ rằng tôi cần phải đọc lên các điểm mã và các cặp thay thế. Cảm ơn!
Hợp đồng của giáo sư Falken vi phạm

6
+1 vì đây dường như là câu trả lời duy nhất đúng cho các ký tự Unicode bên ngoài BMP
Jason S

Đã viết một số mã để minh họa khái niệm lặp qua mã hóa (trái ngược với ký tự): gist.github.com/EmmanuelOga/ phỏng
Emmanuel Oga

26

Tôi đồng ý rằng StringTokenizer là quá mức cần thiết ở đây. Thật ra tôi đã thử những gợi ý trên và dành thời gian.

Thử nghiệm của tôi khá đơn giản: tạo StringBuilder với khoảng một triệu ký tự, chuyển đổi nó thành Chuỗi và duyệt từng chuỗi bằng charAt () / sau khi chuyển đổi thành mảng char / với Bộ tạo ký tự một nghìn lần (tất nhiên là đảm bảo làm một cái gì đó trên chuỗi để trình biên dịch không thể tối ưu hóa toàn bộ vòng lặp :-)).

Kết quả trên Powerbook 2,6 GHz của tôi (đó là mac :-)) và JDK 1.5:

  • Kiểm tra 1: charAt + Chuỗi -> 3138msec
  • Kiểm tra 2: Chuỗi được chuyển đổi thành mảng -> 9568msec
  • Kiểm tra 3: Chuỗi ký tự StringBuilder -> 3536msec
  • Kiểm tra 4: Bộ ký tự và chuỗi ký tự -> 12151msec

Vì kết quả khác nhau đáng kể, cách đơn giản nhất cũng có vẻ là cách nhanh nhất. Thật thú vị, charAt () của StringBuilder dường như chậm hơn một chút so với String.

BTW Tôi đề nghị không sử dụng CharacterIterator vì tôi coi việc lạm dụng ký tự '\ uFFFF' của nó là "kết thúc lặp lại" là một hack thực sự khủng khiếp. Trong các dự án lớn, luôn có hai kẻ sử dụng cùng một loại hack cho hai mục đích khác nhau và mã bị sập thực sự bí ẩn.

Đây là một trong những bài kiểm tra:

    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
Điều này có cùng một vấn đề được nêu ra ở đây: stackoverflow.com/questions/196830/ Kẻ
Emmanuel Oga

22

Trong Java 8, chúng ta có thể giải nó như sau:

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

Phương thức chars () trả về một IntStreamnhư đã đề cập trong doc :

Trả về một luồng int zero - mở rộng các giá trị char từ chuỗi này. Bất kỳ char nào ánh xạ tới một điểm mã thay thế đều được chuyển qua không giải thích được. Nếu chuỗi bị đột biến trong khi luồng đang được đọc, kết quả không được xác định.

Phương thức này codePoints()cũng trả về một IntStreamtài liệu theo:

Trả về một luồng các giá trị điểm mã từ chuỗi này. Bất kỳ cặp thay thế nào gặp phải trong chuỗi đều được kết hợp như thể bởi Character.toCodePoint và kết quả được truyền vào luồng. Bất kỳ đơn vị mã nào khác, bao gồm các ký tự BMP thông thường, các đại diện không ghép cặp và các đơn vị mã không xác định, đều được mở rộng bằng 0 cho các giá trị int sau đó được truyền vào luồng.

Làm thế nào là char và điểm mã khác nhau? Như đã đề cập trong này bài viết:

Unicode 3.1 đã thêm các ký tự bổ sung, nâng tổng số ký tự lên hơn 216 ký tự có thể được phân biệt bằng 16 bit đơn char. Do đó, một chargiá trị không còn có ánh xạ một-một đến đơn vị ngữ nghĩa cơ bản trong Unicode. JDK 5 đã được cập nhật để hỗ trợ tập hợp các giá trị ký tự lớn hơn. Thay vì thay đổi định nghĩa về charloại, một số ký tự bổ sung mới được biểu thị bằng một cặp thay thế gồm hai chargiá trị. Để giảm nhầm lẫn khi đặt tên, một điểm mã sẽ được sử dụng để chỉ số đại diện cho một ký tự Unicode cụ thể, bao gồm cả các ký tự bổ sung.

Cuối cùng tại sao forEachOrderedvà không forEach?

Hành vi của forEachkhông rõ ràng là không forEachOrderedthực hiện khi hành động thực hiện một hành động cho từng thành phần của luồng này, theo thứ tự bắt gặp của luồng nếu luồng có thứ tự bắt gặp được xác định. Vì vậy, forEachkhông đảm bảo rằng thứ tự sẽ được giữ. Ngoài ra kiểm tra câu hỏi này để biết thêm.

Để biết sự khác biệt giữa một ký tự, một điểm mã, glyph và grapheme, hãy kiểm tra câu hỏi này .


21

Có một số lớp dành riêng cho việc này:

import java.text.*;

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

7
Trông giống như một sự quá mức cho một cái gì đó đơn giản như lặp đi lặp lại trên mảng char bất biến.
ddimitrov

1
Tôi không thấy lý do tại sao điều này là quá mức cần thiết. Lặp lại là cách java-ish nhất để làm bất cứ điều gì ... lặp đi lặp lại. StringCharacterIterator bị ràng buộc để tận dụng tối đa tính bất biến.
mỏng

2
Đồng ý với @ddimitrov - đây là quá mức cần thiết. Lý do duy nhất để sử dụng một trình vòng lặp là để tận dụng lợi thế của foreach, dễ "nhìn" hơn một chút so với vòng lặp for. Nếu bạn định viết một vòng lặp thông thường, thì cũng có thể sử dụng charAt ()
Rob Gilliam

3
Sử dụng trình lặp ký tự có lẽ là cách chính xác duy nhất để lặp qua các ký tự, vì Unicode yêu cầu nhiều không gian hơn Java charcung cấp. Một Java charchứa 16 bit và có thể giữ các ký tự Unicode lên U + FFFF nhưng Unicode chỉ định các ký tự lên đến U + 10FFFF. Sử dụng 16 bit để mã hóa kết quả Unicode trong mã hóa ký tự có độ dài thay đổi. Hầu hết các câu trả lời trên trang này đều cho rằng mã hóa Java là mã hóa có độ dài không đổi, điều này là sai.
ceving

3
@ceving Dường như một trình lặp nhân vật sẽ không giúp bạn với các nhân vật không phải là BMP: oracle.com/us/technologists/java/sup
Bruno De Fraine

18

Nếu bạn có Guava trên đường dẫn lớp của bạn, sau đây là một thay thế khá dễ đọc. Quả ổi thậm chí còn có cách thực hiện Danh sách tùy chỉnh khá hợp lý cho trường hợp này, vì vậy điều này không nên không hiệu quả.

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

CẬP NHẬT: Như @Alex đã lưu ý, với Java 8 cũng có CharSequence#charsthể sử dụng. Ngay cả loại là IntStream, vì vậy nó có thể được ánh xạ tới các ký tự như:

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

Nếu bạn cần làm bất cứ điều gì phức tạp thì hãy đi với vòng lặp for + ổi vì bạn không thể thay đổi các biến (ví dụ: Số nguyên và Chuỗi) được xác định bên ngoài phạm vi của forEach bên trong forEach. Bất cứ điều gì bên trong forEach cũng không thể đưa ra các ngoại lệ được kiểm tra, do đó đôi khi cũng gây phiền nhiễu.
sabujp

13

Nếu bạn cần lặp qua các điểm mã của một String(xem câu trả lời này ), cách ngắn hơn / dễ đọc hơn là sử dụng CharSequence#codePointsphương thức được thêm vào trong Java 8:

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

hoặc sử dụng luồng trực tiếp thay vì vòng lặp for:

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

Cũng có CharSequence#charsnếu bạn muốn một luồng các ký tự (mặc dù nó là một IntStream, vì không có CharStream).


3

Tôi sẽ không sử dụng StringTokenizervì đây là một trong các lớp trong JDK đó là di sản.

Javadoc nói:

StringTokenizerlà một lớp kế thừa được giữ lại vì lý do tương thích mặc dù việc sử dụng nó không được khuyến khích trong mã mới. Thay vào đó, mọi người tìm kiếm chức năng này nên sử dụng phương pháp phân chia Stringhoặc java.util.regexgói.


Chuỗi mã thông báo là cách hoàn toàn hợp lệ (và hiệu quả hơn) để lặp lại mã thông báo (nghĩa là các từ trong câu.) Nó chắc chắn là một quá mức cần thiết cho việc lặp lại các ký tự. Tôi đang đánh giá thấp nhận xét của bạn là sai lệch.
ddimitrov

3
ddimitrov: Tôi không theo dõi cách chỉ ra rằng StringTokenizer không được khuyến nghị BAO GỒM một trích dẫn từ JavaDoc ( java.sun.com/javase/6/docs/api/java/util/StringTokenizer.html ) cho nó như vậy là gây hiểu lầm. Nâng cấp để bù đắp.
Powerlord

1
Cảm ơn ông Bemrose ... Tôi cho rằng trích dẫn khối được trích dẫn phải rõ ràng, trong đó người ta có lẽ nên suy luận rằng các bản sửa lỗi hoạt động sẽ không được cam kết với StringTokenizer.
Alan

2

Nếu bạn cần hiệu suất, sau đó bạn phải kiểm tra trên môi trường của bạn. Không con cach nao khac.

Ở đây mã ví dụ:

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

Trên Java trực tuyến tôi nhận được:

1 10349420
2 526130
3 484200
0

Trên API Android x86 17 tôi nhận được:

1 9122107
2 13486911
3 12700778
0

0

Xem Hướng dẫn Java: Chuỗi .

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

Đặt chiều dài vào int lenvà sử dụng forvòng lặp.


1
Tôi bắt đầu cảm thấy hơi spam ... nếu có một từ như vậy :). Nhưng giải pháp này cũng có một vấn đề được nêu ra ở đây: Điều này có cùng một vấn đề được nêu ra ở đây: stackoverflow.com/questions/196830/ trộm
Emmanuel Oga

0

StringTokenizer hoàn toàn không phù hợp với nhiệm vụ phá vỡ một chuỗi thành các ký tự riêng lẻ của nó. Với String#split()bạn có thể làm điều đó một cách dễ dàng bằng cách sử dụng regex không khớp với gì, ví dụ:

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

Nhưng StringTokenizer không sử dụng regexes và không có chuỗi phân cách nào bạn có thể chỉ định sẽ không khớp với gì giữa các ký tự. Có một chút dễ thương hack you có thể sử dụng để thực hiện điều tương tự: sử dụng chuỗi chính nó như là chuỗi delimiter (làm cho mỗi nhân vật trong nó một dấu phân cách) và có nó trả lại delimiters:

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

Tuy nhiên, tôi chỉ đề cập đến các tùy chọn này cho mục đích loại bỏ chúng. Cả hai kỹ thuật này phá vỡ chuỗi ban đầu thành chuỗi một ký tự thay vì nguyên hàm char và cả hai đều liên quan đến rất nhiều chi phí dưới dạng tạo đối tượng và thao tác chuỗi. So sánh điều đó với việc gọi charAt () trong một vòng lặp for, mà hầu như không có chi phí nào.


0

Xây dựng câu trả lời nàycâu trả lời này .

Các câu trả lời ở trên chỉ ra vấn đề của nhiều giải pháp ở đây không lặp lại theo giá trị điểm mã - chúng sẽ gặp rắc rối với bất kỳ ký tự thay thế nào . Các tài liệu java cũng phác thảo vấn đề ở đây (xem "Biểu diễn ký tự Unicode"). Dù sao đi nữa, đây là một số mã sử dụng một số ký tự thay thế thực tế từ bộ Unicode bổ sung và chuyển đổi chúng trở lại thành Chuỗi. Lưu ý rằng .toChars () trả về một mảng ký tự: nếu bạn đang xử lý người thay thế, bạn nhất thiết phải có hai ký tự. Mã này sẽ hoạt động cho bất kỳ ký tự Unicode nào .

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

0

Mã ví dụ này sẽ giúp bạn ra ngoài!

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

Vì vậy, thông thường có hai cách để lặp qua chuỗi trong java đã được trả lời bởi nhiều người ở đây trong chuỗi này, chỉ cần thêm phiên bản của tôi Đầu tiên là sử dụng

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

Nếu hiệu suất bị đe dọa thì tôi sẽ khuyên bạn nên sử dụng cái đầu tiên trong thời gian không đổi, nếu nó không đi với cái thứ hai làm cho công việc của bạn dễ dàng hơn khi xem xét tính bất biến với các lớp chuỗi trong java.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.