Cái nào hiệu quả hơn, một vòng lặp cho mỗi vòng lặp hoặc một vòng lặp?


206

Đó là cách hiệu quả nhất để đi qua một bộ sưu tập?

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a) {
  integer.toString();
}

hoặc là

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
   Integer integer = (Integer) iterator.next();
   integer.toString();
}

Xin lưu ý rằng đây không phải là một bản sao chính xác của cái này , cái này , cái này hay cái này , mặc dù một trong những câu trả lời cho câu hỏi cuối cùng đã đến gần. Lý do đây không phải là bản sao, vì hầu hết trong số này đang so sánh các vòng lặp mà bạn gọi get(i)bên trong vòng lặp, thay vì sử dụng trình vòng lặp.

Theo đề xuất trên Meta, tôi sẽ đăng câu trả lời của mình cho câu hỏi này.


Tôi nghĩ nó không tạo ra sự khác biệt vì Java của nó và cơ chế tạo khuôn mẫu ít hơn một chút so với đường cú pháp
Hassan Syed


2
@OMG Ponies: Tôi không tin rằng đây là một bản sao, vì nó không so sánh vòng lặp với trình vòng lặp, mà hỏi tại sao các bộ sưu tập trả về các trình vòng lặp, thay vì có các trình vòng lặp trực tiếp trên lớp.
Paul Wagland

Câu trả lời:


263

Nếu bạn chỉ đi lang thang trên bộ sưu tập để đọc tất cả các giá trị, thì không có sự khác biệt giữa việc sử dụng một cú pháp lặp hoặc cú pháp vòng lặp mới, vì cú pháp mới chỉ sử dụng trình lặp dưới nước.

Tuy nhiên, nếu bạn có nghĩa là bằng cách lặp vòng lặp "c-style" cũ:

for(int i=0; i<list.size(); i++) {
   Object o = list.get(i);
}

Sau đó, vòng lặp for mới, hoặc iterator, có thể hiệu quả hơn rất nhiều, tùy thuộc vào cấu trúc dữ liệu cơ bản. Lý do cho điều này là đối với một số cấu trúc dữ liệu, get(i)là một hoạt động O (n), làm cho vòng lặp hoạt động O (n 2 ). Một danh sách liên kết truyền thống là một ví dụ về cấu trúc dữ liệu như vậy. Tất cả các trình vòng lặp có một yêu cầu cơ bản next()phải là phép toán O (1), tạo vòng lặp O (n).

Để xác minh rằng trình lặp được sử dụng dưới nước theo cú pháp vòng lặp for mới, hãy so sánh các mã byte được tạo từ hai đoạn mã Java sau. Đầu tiên là vòng lặp for:

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a)
{
  integer.toString();
}
// Byte code
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 3
 GOTO L2
L3
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 2 
 ALOAD 2
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L2
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L3

Và thứ hai, trình vòng lặp:

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
  Integer integer = (Integer) iterator.next();
  integer.toString();
}
// Bytecode:
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 2
 GOTO L7
L8
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 3
 ALOAD 3
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L7
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L8

Như bạn có thể thấy, mã byte được tạo giống hệt nhau một cách hiệu quả, do đó không có hình phạt hiệu năng nào khi sử dụng một trong hai hình thức. Do đó, bạn nên chọn hình thức vòng lặp hấp dẫn nhất về mặt thẩm mỹ đối với bạn, đối với hầu hết mọi người sẽ là vòng lặp cho mỗi vòng lặp, vì điều đó có ít mã nồi hơi hơn.


4
Tôi tin rằng anh ấy đã nói ngược lại, rằng foo.get (i) có thể kém hiệu quả hơn rất nhiều. Hãy nghĩ về LinkedList. Nếu bạn thực hiện một foo.get (i) ở giữa LinkedList, nó phải đi qua tất cả các nút trước đó để đến i. Mặt khác, một trình vòng lặp sẽ giữ một điều khiển cho cấu trúc dữ liệu cơ bản và sẽ cho phép bạn đi qua các nút một lần.
Michael Krauklis

1
Không phải là một vấn đề lớn nhưng một for(int i; i < list.size(); i++) {vòng lặp kiểu cũng phải đánh giá list.size()vào cuối mỗi lần lặp, nếu nó được sử dụng đôi khi hiệu quả hơn để lưu trữ kết quả của list.size()lần đầu tiên.
Brett Ryan

3
Trên thực tế, câu lệnh gốc cũng đúng với trường hợp của ArrayList và tất cả các câu lệnh khác thực hiện giao diện RandomAccess. Vòng lặp "kiểu C" nhanh hơn vòng lặp dựa trên Iterator. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
Một lý do để sử dụng vòng lặp kiểu C cũ thay vì cách tiếp cận Iterator, bất kể đó là phiên bản foreach hay desugar'd, là rác. Nhiều cấu trúc dữ liệu khởi tạo một Iterator mới khi .iterator () được gọi, tuy nhiên chúng có thể được truy cập miễn phí phân bổ bằng cách sử dụng vòng lặp kiểu C. Điều này có thể quan trọng trong một số môi trường hiệu suất cao nhất định nơi người ta đang cố gắng tránh (a) nhấn vào bộ cấp phát hoặc (b) bộ sưu tập rác.
Dan

3
Cũng như một nhận xét khác, đối với ArrayLists, vòng lặp for (int i = 0 ....) nhanh hơn khoảng 2 lần so với sử dụng iterator hoặc phương pháp for (:), vì vậy nó thực sự phụ thuộc vào cấu trúc cơ bản. Và như một lưu ý phụ, lặp lại HashSets cũng rất tốn kém (nhiều hơn một Danh sách mảng), vì vậy hãy tránh những thứ như bệnh dịch hạch (nếu bạn có thể).
Leo

106

Sự khác biệt không phải ở hiệu suất, mà là khả năng. Khi sử dụng một tham chiếu trực tiếp, bạn có nhiều quyền lực hơn bằng cách sử dụng một loại trình vòng lặp (ví dụ List.iterator () so với List.listIterator (), mặc dù trong hầu hết các trường hợp, chúng trả về cùng một triển khai). Bạn cũng có khả năng tham chiếu Iterator trong vòng lặp của mình. Điều này cho phép bạn thực hiện những việc như xóa các mục khỏi bộ sưu tập của mình mà không nhận được đồng thờiExceptionModificationException.

ví dụ

Không sao đâu

Set<Object> set = new HashSet<Object>();
// add some items to the set

Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
     Object o = setIterator.next();
     if(o meets some condition){
          setIterator.remove();
     }
}

Đây không phải là, vì nó sẽ đưa ra một ngoại lệ sửa đổi đồng thời:

Set<Object> set = new HashSet<Object>();
// add some items to the set

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}

12
Điều này rất đúng, mặc dù nó không trả lời trực tiếp câu hỏi mà tôi đã đưa ra +1 vì nó mang tính thông tin và trả lời câu hỏi tiếp theo hợp lý.
Paul Wagland

1
Có, chúng ta có thể truy cập các phần tử bộ sưu tập bằng vòng lặp foreach, nhưng chúng ta không thể loại bỏ chúng, nhưng chúng ta có thể loại bỏ các phần tử bằng Iterator.
Akash5288

22

Để mở rộng câu trả lời của Paul, anh ta đã chứng minh rằng mã byte giống nhau trên trình biên dịch cụ thể đó (có lẽ là javac của Sun?) Nhưng các trình biên dịch khác nhau không được đảm bảo để tạo cùng mã byte, phải không? Để xem sự khác biệt thực sự giữa hai loại này, hãy đi thẳng vào nguồn và kiểm tra Đặc tả ngôn ngữ Java, cụ thể là 14,14.2, "Bản nâng cao cho câu lệnh" :

Câu forlệnh nâng cao tương đương với forcâu lệnh cơ bản có dạng:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

Nói cách khác, JLS yêu cầu hai cái này tương đương nhau. Về lý thuyết, điều đó có thể có nghĩa là sự khác biệt biên trong mã byte, nhưng trong thực tế, vòng lặp for được tăng cường là bắt buộc để:

  • Gọi .iterator()phương thức
  • Sử dụng .hasNext()
  • Làm cho biến cục bộ có sẵn thông qua .next()

Vì vậy, nói cách khác, đối với tất cả các mục đích thực tế, mã byte sẽ giống hệt nhau hoặc gần giống nhau. Thật khó để dự tính bất kỳ triển khai trình biên dịch nào sẽ dẫn đến bất kỳ sự khác biệt đáng kể nào giữa hai trình biên dịch.


Trên thực tế, thử nghiệm tôi đã làm là với trình biên dịch Eclipse, nhưng điểm chung của bạn vẫn đứng vững. +1
Paul Wagland

3

Phần foreachdưới đang tạoiterator , gọi hasNext () và gọi next () để nhận giá trị; Vấn đề với hiệu suất chỉ xảy ra nếu bạn đang sử dụng thứ gì đó thực hiện RandomomAccess.

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
   CustomObj custObj = iter.next();
   ....
}

Các vấn đề về hiệu năng với vòng lặp dựa trên iterator là bởi vì nó là:

  1. phân bổ một đối tượng ngay cả khi danh sách trống ( Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() trong mỗi lần lặp của vòng lặp, có một cuộc gọi ảo invokeInterface (đi qua tất cả các lớp, sau đó thực hiện tra cứu bảng phương thức trước khi nhảy).
  3. việc thực hiện trình lặp phải thực hiện ít nhất 2 trường tra cứu để thực hiện hasNext()cuộc gọi có giá trị: # 1 có được số hiện tại và # 2 có được tổng số
  4. bên trong vòng lặp cơ thể, có một cuộc gọi ảo invokeInterface khác iter.next(vì vậy: đi qua tất cả các lớp và thực hiện tra cứu bảng phương thức trước khi nhảy) và cũng phải thực hiện tra cứu các trường: # 1 lấy chỉ mục và # 2 lấy tham chiếu đến mảng để thực hiện bù vào nó (trong mỗi lần lặp).

Tối ưu hóa tiềm năng là chuyển sang mộtindex iteration tra cứu kích thước bộ nhớ cache:

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

Ở đây chúng tôi có:

  1. một cuộc gọi phương thức ảo invokeInterface customList.size()trên việc tạo ban đầu của vòng lặp for để có được kích thước
  2. cuộc gọi phương thức get customList.get(x)trong vòng lặp thân cho vòng lặp, đó là một trường tra cứu mảng và sau đó có thể thực hiện bù vào mảng

Chúng tôi giảm một tấn các cuộc gọi phương thức, tra cứu trường. Điều này bạn không muốn làm với LinkedListhoặc với một cái gì đó không phải là một RandomAccessbộ sưu tập obj, nếu không thì customList.get(x)nó sẽ biến thành thứ gì đó phải vượt qua LinkedListmỗi lần lặp.

Điều này là hoàn hảo khi bạn biết rằng đó là bất kỳ RandomAccessbộ sưu tập danh sách dựa trên.


1

foreachsử dụng iterators dưới mui xe nào. Nó thực sự chỉ là cú pháp đường.

Hãy xem xét chương trình sau:

import java.util.List;
import java.util.ArrayList;

public class Whatever {
    private final List<Integer> list = new ArrayList<>();
    public void main() {
        for(Integer i : list) {
        }
    }
}

Chúng ta hãy biên dịch nó với javac Whatever.java,
và đọc mã byte được phân tách main()bằng cách sử dụng javap -c Whatever:

public void main();
  Code:
     0: aload_0
     1: getfield      #4                  // Field list:Ljava/util/List;
     4: invokeinterface #5,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
     9: astore_1
    10: aload_1
    11: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
    16: ifeq          32
    19: aload_1
    20: invokeinterface #7,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    25: checkcast     #8                  // class java/lang/Integer
    28: astore_2
    29: goto          10
    32: return

Chúng ta có thể thấy rằng foreachbiên dịch thành một chương trình:

  • Tạo iterator bằng cách sử dụng List.iterator()
  • Nếu Iterator.hasNext(): gọi Iterator.next()và tiếp tục vòng lặp

Đối với "tại sao vòng lặp vô dụng này không được tối ưu hóa khỏi mã được biên dịch? Chúng ta có thể thấy rằng nó không làm gì với mục danh sách": tốt, bạn có thể mã hóa lần lặp của mình để .iterator()có tác dụng phụ hoặc do đó .hasNext()có tác dụng phụ hoặc hậu quả có ý nghĩa.

Bạn có thể dễ dàng tưởng tượng rằng một lần lặp đại diện cho một truy vấn có thể cuộn từ cơ sở dữ liệu có thể làm điều gì đó kịch tính .hasNext()(như liên hệ với cơ sở dữ liệu hoặc đóng con trỏ vì bạn đã đạt đến cuối tập kết quả).

Vì vậy, mặc dù chúng tôi có thể chứng minh rằng không có gì xảy ra trong cơ thể vòng lặp, nó đắt hơn (khó hiểu?) Để chứng minh rằng không có gì có ý nghĩa / hậu quả xảy ra khi chúng tôi lặp đi lặp lại. Trình biên dịch phải để phần thân vòng lặp trống này trong chương trình.

Điều tốt nhất chúng ta có thể hy vọng sẽ là một cảnh báo trình biên dịch . Thật thú vị khi javac -Xlint:all Whatever.javanào không cảnh báo cho chúng tôi về thân vòng lặp trống này. IntelliJ IDEA mặc dù. Phải thừa nhận rằng tôi đã cấu hình IntelliJ để sử dụng Trình biên dịch Eclipse, nhưng đó có thể không phải là lý do tại sao.

nhập mô tả hình ảnh ở đây


0

Iterator là một giao diện trong khung công tác Bộ sưu tập Java cung cấp các phương thức để duyệt qua hoặc lặp lại trên một bộ sưu tập.

Cả iterator và for loop đều hoạt động tương tự nhau khi động lực của bạn là chỉ lướt qua một bộ sưu tập để đọc các phần tử của nó.

for-each chỉ là một cách để lặp lại trên Bộ sưu tập.

Ví dụ:

List<String> messages= new ArrayList<>();

//using for-each loop
for(String msg: messages){
    System.out.println(msg);
}

//using iterator 
Iterator<String> it = messages.iterator();
while(it.hasNext()){
    String msg = it.next();
    System.out.println(msg);
}

Và cho mỗi vòng lặp chỉ có thể được sử dụng trên các đối tượng thực hiện giao diện iterator.

Bây giờ trở lại trường hợp của vòng lặp for và vòng lặp.

Sự khác biệt đến khi bạn cố gắng sửa đổi một bộ sưu tập. Trong trường hợp này, iterator hiệu quả hơn vì thuộc tính không nhanh . I E. nó kiểm tra bất kỳ sửa đổi nào trong cấu trúc của bộ sưu tập cơ bản trước khi lặp qua phần tử tiếp theo. Nếu có bất kỳ sửa đổi nào được tìm thấy, nó sẽ ném ra ConcurrencyModificationException .

(Lưu ý: Chức năng này của iterator chỉ có thể áp dụng trong trường hợp các lớp bộ sưu tập trong gói java.util. Nó không áp dụng cho các bộ sưu tập đồng thời vì bản chất chúng không an toàn)


1
Tuyên bố của bạn về sự khác biệt là không đúng, đối với mỗi vòng lặp cũng sử dụng một trình vòng lặp dưới nước và do đó có hành vi tương tự.
Paul Wagland

@Pault Wagland, tôi đã sửa đổi câu trả lời của mình, cảm ơn bạn đã chỉ ra lỗi sai
lập

cập nhật của bạn vẫn chưa chính xác. Hai đoạn mã mà bạn có được xác định bởi ngôn ngữ là giống nhau. Nếu có bất kỳ sự khác biệt trong hành vi là một lỗi trong việc thực hiện. Sự khác biệt duy nhất là bạn có quyền truy cập vào trình vòng lặp hay không.
Paul Wagland

@Paul Wagland Ngay cả khi bạn sử dụng cài đặt mặc định cho mỗi vòng lặp sử dụng trình vòng lặp, nó vẫn sẽ ném và ngoại lệ nếu bạn cố gắng sử dụng phương thức remove () trong các hoạt động đồng thời. Kiểm tra các thông tin sau để biết thêm thông tin tại đây
lập dị

1
với mỗi vòng lặp, bạn không có quyền truy cập vào trình vòng lặp, vì vậy bạn không thể gọi gỡ bỏ nó. Nhưng đó là vấn đề bên cạnh, trong câu trả lời của bạn, bạn cho rằng một cái là an toàn trong khi cái kia thì không. Theo đặc tả ngôn ngữ, chúng tương đương nhau, vì vậy cả hai chỉ là luồng an toàn như các bộ sưu tập cơ bản.
Paul Wagland

-8

Chúng ta nên tránh sử dụng vòng lặp truyền thống trong khi làm việc với Bộ sưu tập. Lý do đơn giản mà tôi sẽ đưa ra là độ phức tạp của vòng lặp for là thứ tự O (sqr (n)) và độ phức tạp của Iterator hoặc thậm chí vòng lặp for được tăng cường chỉ là O (n). Vì vậy, nó mang lại sự khác biệt về hiệu suất .. Chỉ cần lấy một danh sách khoảng 1000 mặt hàng và in nó bằng cả hai cách. và cũng in chênh lệch thời gian để thực hiện. Bạn có thể thấy sự khác biệt.


vui lòng thêm một số ví dụ minh họa để hỗ trợ báo cáo của bạn.
Rajesh Pitty

@Chandan Xin lỗi nhưng những gì bạn viết là sai. Ví dụ: std :: vector cũng là một bộ sưu tập nhưng chi phí truy cập của nó là O (1). Vì vậy, một vòng lặp truyền thống trên một vectơ chỉ là O (n). Tôi nghĩ rằng bạn muốn nói, nếu quyền truy cập của vùng chứa bên dưới có chi phí truy cập là O (n), thì đó là cho danh sách std ::, hơn là có độ phức tạp của O (n ^ 2). Sử dụng các trình vòng lặp trong trường hợp đó sẽ giảm chi phí xuống O (n), bởi vì các trình vòng lặp cho phép truy cập trực tiếp vào các phần tử.
kaiser

Nếu bạn thực hiện tính toán chênh lệch thời gian, hãy đảm bảo cả hai bộ được sắp xếp (hoặc phân phối ngẫu nhiên khá phân loại) và chạy thử nghiệm hai lần cho mỗi bộ và chỉ tính lần chạy thứ hai của mỗi bộ. Kiểm tra lại thời gian của bạn một lần nữa với điều này (đó là một lời giải thích dài về lý do tại sao bạn cần chạy thử nghiệm hai lần). Bạn cần chứng minh (có lẽ với mã) làm thế nào là đúng. Mặt khác, theo như tôi biết cả hai đều giống nhau về hiệu suất, nhưng không phải là khả năng.
ydobonebi
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.