it-source

Java 8 Itable.ForEach()와 foreach 루프

criticalcode 2022. 11. 29. 21:43
반응형

Java 8 Itable.ForEach()와 foreach 루프

다음 중 Java 8에서 더 나은 방법은 무엇입니까?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:

for (String join : joins) {
    mIrc.join(mSession, join);
}

람다로 '간단화'할 수 있는 루프는 많이 있습니다만, 실제로 사용할 수 있는 장점이 있을까요?퍼포먼스와 가독성이 향상됩니까?

편집

이 질문도 더 긴 방법으로 확장하겠습니다.람다에서 모함수를 되돌리거나 깰 수 없다는 것은 알고 있습니다만, 이것 또한 비교 시 고려해야 할 사항입니다만, 그 외에 고려해야 할 사항이 있습니까?

좋은 은 '하다'를 사용하는 입니다.for-eachKeep It Simple, Stuffy, New-flanded 원칙을 위반하는 것 외에forEach()에는 적어도 다음과 같은 결함이 있습니다.

  • 최종 변수가 아닌 변수는 사용할 수 없습니다.따라서 다음과 같은 코드는 각 람다에 대해 a로 변환할 수 없습니다.
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • 선택한 예외를 처리할 수 없습니다.람다는 실제로 체크된 예외를 던지는 것이 금지되어 있지 않지만 다음과 같은 일반적인 기능 인터페이스입니다.Consumer신고하지 마세요.체크된 는 해당 를 "예외하다로 .try-catch ★★★★★★★★★★★★★★★★★」Throwables.propagate()하지만 그렇게 해도 예외 발생이 항상 명확하지는 않습니다.forEach()

  • 흐름 제어가 제한되어 있습니다.areturn람다에서는 a와 같다continue각각에 대해서, 그러나 에 상당하는 것은 없다.break또한 반환값, 단락, 플래그 설정 등의 작업을 수행하는 것도 어렵습니다(최종 변수 없음 규칙 위반이 아니라면 약간 완화되었을 것입니다)."이것은 단순한 최적화가 아니라 일부 시퀀스(파일 내의 행 읽기 등)에 부작용이 있거나 무한 시퀀스가 있을 수 있다는 점을 고려할 때 매우 중요합니다."

  • 병렬로 실행될 수 있습니다. 최적화가 필요한 코드 0.1%를 제외한 모든 사용자에게 끔찍한 일입니다.모든 병렬 코드는 (기존 멀티 스레드 실행의 잠금, 휘발성 및 기타 특히 불쾌한 측면을 사용하지 않더라도) 자세히 검토해야 합니다.어떤 버그라도 찾기가 어렵습니다.

  • JIT는 플레인 루프와 같은 범위까지 Each()+lambda를 최적화할 수 없기 때문에 퍼포먼스가 저하될 수 있습니다.특히 람다가 새로운 경우에는 더욱 그렇습니다.최적화란 lamda 호출에 따른 오버헤드가 아니라 최신 JIT 컴파일러가 실행 중인 코드에 대해 수행하는 정교한 분석과 변환을 의미합니다.

  • 병렬 처리가 필요한 경우 실행 서비스를 사용하는 것이 훨씬 빠르고 어렵지 않을 수 있습니다.스트림은 자동(읽기: 문제에 대해 잘 알지 못함)과 특수(읽기: 일반적인 경우 비효율적) 병렬화 전략(fork-join 재귀 분해)을 모두 사용합니다.

  • 계층이 중첩되어 병렬 실행이 금지되어 있기 때문에 디버깅이 더욱 복잡해집니다.디버거는 주변 코드의 변수를 표시하는 데 문제가 있을 수 있으며 스텝스루와 같은 기능이 예상대로 작동하지 않을 수 있습니다.

  • 일반적으로 스트림은 코드화, 읽기 및 디버깅이 어렵습니다.실제로 이는 복잡한 "유동" API에 일반적으로 해당됩니다.복잡한 단일 문장의 조합, 제네릭의 과도한 사용 및 중간 변수의 결여로 인해 혼란스러운 오류 메시지가 생성되고 디버깅이 중단됩니다."이 메서드에는 타입 X에 오버로드가 없습니다"가 아니라 "어디서 타입을 망쳤는데 어디서 어떻게 했는지 알 수 없습니다"에 가까운 오류 메시지가 나타납니다.마찬가지로 코드가 여러 문장으로 분할되어 중간값이 변수에 저장될 때처럼 디버거 내의 사물을 쉽게 단계별로 살펴보고 검토할 수 없습니다.마지막으로 코드를 읽고 각 실행 단계에서 유형 및 동작을 이해하는 것은 간단하지 않을 수 있습니다.

  • 에 잘 띄네Java 언어에는 이미 for-each 문이 있습니다.함수 호출로 대체해야 하는 이유왜 표현의 어딘가에 부작용을 숨기도록 부추기는가?왜 다루기 힘든 외줄타기를 장려하는가?일반과 새 것을 혼합하는 것은 좋지 않은 스타일이다.코드는 숙어(반복으로 이해하기 빠른 패턴)로 말해야 하며, 사용되는 숙어가 적을수록 코드가 명확해지고 어떤 숙어를 사용할지 결정하는 데 걸리는 시간이 줄어듭니다(저와 같은 완벽주의자에게는 큰 시간 낭비입니다!).

보시다시피 저는 ForEach()의 광팬이 아닙니다.단, 이치에 맞는 경우는 제외합니다.

특히 나를 불쾌하게 하는 것은Stream does does does does 。Iterable 메서드가 )iterator에서는 할 수 또, Each()의 Each()에 만 사용할 수 있습니다.하는 것을 합니다.(Iterable<T>)stream::iterator더 나은 대안은 StreamEx를 사용하는 것입니다.StreamEx를 사용하면 Stream API의 문제를 해결할 수 있습니다.Iterable.

할 수 없다forEach()는 다음합니다.

  • 동기화된 목록에 대해 원자적으로 반복됩니다.그 전에 리스트는 다음과 같이 생성되었습니다.Collections.synchronizedList()set 등에 set.의 set은 .

  • 병렬 실행(적절한 병렬 스트림을 사용).따라서 Streams 및 Spliterator에 내장된 성능 전제 조건과 문제가 일치하는 경우 Executor Service를 사용하는 경우보다 몇 줄의 코드를 절약할 수 있습니다.

  • 동기화된 리스트와 같이 반복을 제어하는 것으로부터 이익을 얻는 특정 컨테이너(다만, 사람들이 더 많은 예를 제시하지 않는 한, 이것은 대체로 이론적이다)

  • 를 사용하여 단일 함수를 보다 깔끔하게 호출할 수 있습니다.forEach() 메서드 인수 " " " " " " ) 、 " ( " )list.forEach (obj::someMethod)단, 체크된 예외, 더 어려운 디버깅, 코드 작성 시 사용하는 관용어 수 감소에 관한 점에 유의하십시오.

참고 자료:

편집: 람다(http://www.javac.info/closures-v06a.html Google Cache 등)의 최초 제안 중 일부는 제가 언급한 몇 가지 문제를 해결한 것 같습니다(물론 그 자체의 복잡성을 추가하면서).

이러한 이점은 작업을 병렬로 실행할 수 있는 경우에 고려됩니다.(http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - 내부 및 외부 반복에 대한 섹션 참조)

  • 내 관점에서 가장 큰 장점은 루프 내에서 수행해야 할 작업의 구현을 병렬 또는 순차적으로 실행할지 결정할 필요 없이 정의할 수 있다는 것이다.

  • 루프가 병렬로 실행되도록 하려면 간단히 다음을 작성할 수 있습니다.

     joins.parallelStream().forEach(join -> mIrc.join(mSession, join));
    

    스레드 처리 등을 위해 추가 코드를 작성해야 합니다.

주의: 제 답변으로, 저는 다음 명령어를 구현에 참여한다고 가정했습니다.java.util.Stream인터페이스입니다.가 「(join)」만 경우.java.util.Iterableinterface 이 interface interface interface interface 。

'그러다', '그러다', '라는 인상을 받을 수 있어요.Iterable#forEach람다 식과 조합하는 것은 기존의 각 루프에 대한 바로 가기/대체입니다.이치노OP > 음 :

joins.forEach(join -> mIrc.join(mSession, join));

글을 쓰는 지름길이 아닙니다.

for (String join : joins) {
    mIrc.join(mSession, join);
}

이런 식으로 사용해서는 안 됩니다.대신, 이것은 (정확히 동일하지는 않지만) 쓰기의 단축키입니다.

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

다음 Java 7 코드를 대체하는 것입니다.

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

위의 예시와 같이 루프 본체를 기능 인터페이스로 치환하면 코드가 보다 명확해집니다.즉, (1) 루프의 본체는 주변 코드와 제어 흐름에 영향을 주지 않으며 (2) 루프의 본체는 주변 코드에 영향을 주지 않고 다른 기능의 구현으로 대체될 수 있습니다.외부 스코프의 비최종 변수에 액세스할 수 없는 것은 함수/램브다 부족이 아니라 의 의미를 구별하는 기능입니다.Iterable#forEach기존의 각 루프의 의미로부터 해방됩니다.「」의 ,Iterable#forEach코드에 대한 추가 정보를 즉시 얻을 수 있기 때문에 코드를 보다 쉽게 읽을 수 있습니다.

Java에서는 기존의 각 루프에 대한 모범 사례('베스트 프랙티스'라는 용어가 과도하게 사용되지 않도록 하기 위해)가 유지됩니다.하지만 그렇다고 해서Iterable#forEach나쁜 관행이나 나쁜 스타일로 여겨져야 한다.을 수행하는 것이 또, 에는 기존의 각하는 것도 됩니다.Iterable#forEach그게 말이 되는 곳이지

의의 of의 Iterable#forEach된 바와 같이, 를 들 수 .여기서 몇 가지 이유를 설명하겠습니다.Iterable#forEach:

  • 코드를 더 명확하게 하려면:상기와 같이Iterable#forEach 는 상황에 따라 코드를 보다 명확하게 읽을 수 있도록 합니다.

  • 코드의 확장성과 유지보수를 향상시키려면 다음 절차를 따릅니다.함수를 루프의 본체로 사용하면 이 함수를 다른 구현으로 대체할 수 있습니다(전략 패턴 참조).예를 들어, 람다 식을 하위 클래스가 덮어쓸 수 있는 메서드 호출로 쉽게 대체할 수 있습니다.

    joins.forEach(getJoinStrategy());
    

    그런 다음 기능 인터페이스를 구현하는 열거형을 사용하여 기본 전략을 제공할 수 있습니다.이로 인해 코드의 확장성이 향상될 뿐만 아니라 루프 구현과 루프 선언이 분리되기 때문에 유지보수성도 향상됩니다.

  • 코드를 더 디버깅하려면:루프 실장을 선언에서 분리하면 디버깅을 보다 쉽게 할 수 있습니다.이는 디버깅메시지를 출력하는 특수한 디버깅 실장이 가능하기 때문입니다.메인 코드와 디버깅메시지를 복잡하게 할 필요는 없습니다.if(DEBUG)System.out.println()디버깅 실장은 예를 들어 실제 기능 실장을 장식하는 위임자가 될 수 있습니다.

  • 퍼포먼스에 중요한 코드를 최적화하려면:이 문맥의 일부 주장과는 달리Iterable#forEach 는 적어도 ArrayList를 사용하여 Hotspot을 "-client" 모드로 실행하고 있는 경우, 각 루프에 대해 기존의 퍼포먼스보다 뛰어난 성능을 제공합니다.이러한 성능 향상은 대부분의 사용 사례에서 미미하고 무시할 수 있는 수준이지만, 이러한 추가 성능으로 인해 달라질 수 있는 상황이 있습니다.예: 라이브러리 유지관리자는 기존 루프 구현의 일부를 다음과 같이 교체해야 하는지 여부를 평가하고 싶어할 것입니다.Iterable#forEach.

    이 진술을 사실로 뒷받침하기 위해 캘리퍼와 함께 몇 가지 마이크로 벤치마크를 했습니다.테스트 코드는 다음과 같습니다(git의 최신 캘리퍼 필요).

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }
    

    결과는 다음과 같습니다.

    "- "-client"와 함께 Iterable#forEach는 ArrayList에서 기존의 for 루프를 능가하지만 어레이에서 직접 반복하는 것보다 여전히 느립니다."-은 거의 합니다."-server"는 "-server"를 사용합니다.

  • 병렬 실행을 선택적으로 지원하려면:여기에서는 이미 언급되어 있습니다.기능 인터페이스를 실행할 수 있습니다.Iterable#forEach스트림을 병행해서 사용하는 것은 확실히 중요한 측면입니다.부터Collection#parallelStream()는 루프가 실제로 병렬로 실행된다는 것을 보증하지 않습니다.이것은 옵션 기능으로 간주할 필요가 있습니다.목록을 반복함으로써list.parallelStream().forEach(...);다음과 같이 명시적으로 말합니다.루프는 병렬 실행을 지원하지만 이 루프에 종속되지는 않습니다.다시 말하지만, 이것은 기능이지 결점이 아닙니다.

    병렬 실행에 대한 결정을 실제 루프 구현에서 멀어지는 것으로 코드 자체에 영향을 주지 않고 선택적으로 코드를 최적화할 수 있습니다.이것은 좋은 일입니다.또한 기본 병렬 스트림 구현이 사용자의 요구에 맞지 않는 경우, 누구도 사용자가 자체 구현을 제공하는 것을 막지 않습니다.예를 들어 기본 운영 체제, 수집 크기, 코어 수 및 일부 기본 설정에 따라 최적화된 수집을 제공할 수 있습니다.

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }
    

    여기서 좋은 점은 루프 구현이 이러한 세부 사항을 알 필요도 없고 신경 쓸 필요도 없다는 것입니다.

forEach()될 수 있습니다.는 표준 때문입니다.이는 표준 반복 방법이 아니라 반복 가능한 요소가 최적의 반복 방법을 알고 있기 때문입니다.따라서 내부 루프 또는 외부 루프와 같은 차이가 있습니다.

를 들어, 「」입니다.ArrayList.forEach(action) 간히 may may may may may may may may may may may may라고 할 수 .

for(int i=0; i<size; i++)
    action.accept(elements[i])

많은 발판을 필요로 하는 각 루프에 비해

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

2개의 의 오버헤드 비용으로 할 필요가 .forEach()하나는 람다 오브젝트를 만들고 다른 하나는 람다 메서드를 호출하는 것입니다.그것들은 아마 중요하지 않을 것이다.

또한 다양한 사용 사례에 대한 내부/외부 반복 비교에 대해서는 http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/를 참조하십시오.

TL;DR:List.stream().forEach()가장 빨랐어요.

벤치마킹 반복의 결과를 추가해야겠다고 생각했습니다.매우 간단한 접근법(벤치마크 프레임워크 없음)을 사용하여 5가지 다른 방법을 벤치마킹했습니다.

  1. 한 ★★★★★for
  2. 클래식 포어치
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

테스트 절차 및 파라미터

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

되어야 하며 어느 정도의 이 있어야 .doIt(Integer i)을 세 번 합니다.메인 클래스에서 테스트된 방법을 3번 실행하여 JVM을 예열합니다. 각 방법에 해서 테스트 합니다.System.nanoTime()§:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

이것은 Java 버전 1.8.0_05의 i5 4 코어 CPU에서 실행했습니다.

한 ★★★★★for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

실행 시간: 4.21 ms

클래식 포어치

for(Integer i : list) {
    doIt(i);
}

실행 시간: 5.95 ms

List.forEach()

list.forEach((i) -> doIt(i));

실행 시간: 3.11 ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

실행 시간: 2.79 ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

실행 시간: 3.6 ms

좀 더 설명을 해드려야 될 것 같아서...

패러다임에 대하여\style

그게 아마 가장 주목할 만한 점일 거예요.부작용을 피할 수 있는 것 때문에 FP가 인기를 끌게 되었다.이 질문과는 관련이 없기 때문에, 여기서 얻을 수 있는 장점에 대해서는 자세히 설명하지 않겠습니다.

다만, Itable을 사용한 반복은, 각각 FP로부터 영감을 받아 Java에 FP를 더 많이 가져오는 결과라고 말할 수 있습니다(아이러니컬하게도, 순수 FP에서는 부작용의 도입 이외에는 각 FP에 별로 도움이 되지 않습니다).

결국 당신이 현재 쓰고 있는 것은 취향의 문제라고 말하고 싶습니다.

병렬에 대해서요.

퍼포먼스의 관점에서 보면, 각각에 대해서, Itable.를 사용하는 것으로부터, 약속된 현저한 메리트는 없습니다.

Itable.에 관한 공식 문서에 따르면 각각:

모든 요소가 처리되거나 작업이 예외를 발생시킬 때까지 반복할 때 요소가 발생하는 순서대로 반복 가능한 내용에 대해 지정된 작업을 수행합니다.

암묵적인 병행은 없을 거라는 게 의사들의 견해입니다1개를 추가하면 LSP 위반이 됩니다.

Java 8에서는 "병렬 컬렉션"이 약속되어 있지만 이러한 컬렉션과 함께 작업하려면 좀 더 명시적이고 신중하게 사용해야 합니다(예를 들어 mschenk74의 답변 참조).

BTW: 이 경우 스트림.각각이 사용되며, 실제 작업이 병렬로 수행된다는 보장은 없습니다(기본 수집에 따라 다름).

업데이트: 한 눈에 보기엔 그리 명확하지 않고 약간 과장되어 있을 수 있지만 스타일과 가독성 측면의 또 다른 측면이 있습니다.

우선, 담백하고 오래된 포루프는 담백하고 오래되었다.다들 이미 알고 있어요

둘째, 더 중요한 것은 Itable을 사용하는 것입니다.각각 1인용 람다만 사용하실 수 있습니다.만약 "몸"이 무거워진다면-그렇게 읽을 수 없는 경향이 있습니다.여기서는 내부 클래스(우크)를 사용하거나 플레인 올드 forloop을 사용하는 두 가지 옵션이 있습니다.같은 코드베이스에서 같은 일(컬렉션에 대한 반복)이 다양한 베이/스타일을 하고 있는 것을 보면 짜증이 나는 경우가 많은데, 이것이 사실인 것 같습니다.

다시 말씀드리지만, 이것은 문제가 될 수도 있고 아닐 수도 있습니다.코드 작업하는 사람에 따라 다르죠

업라이징된 중 forEach님의 제한사항은 체크된 예외 지원이 없다는 것입니다.

생각할 수 있는 회피책 중 하나는 단말기를 교체하는 것입니다.forEach"CHANGE: "CHANGE: " 。

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

다음은 람다 및 스트림 내에서 체크된 예외 처리에 대한 기타 회피책과 함께 가장 자주 묻는 질문 목록입니다.

예외를 발생시키는 Java 8 Lambda 함수?

Java 8: Lambda-Streams, 메서드로 필터링(예외 포함)

Java 8 스트림 내에서 CHECKED 예외를 슬로우하려면 어떻게 해야 합니까?

Java 8: 람다 식에서 선택된 필수 예외 처리입니다.왜 선택사항이 아닌 필수사항일까요?

Java 1.8 for 1.7 for loop보다 강화된 각 메서드의 장점은 코드를 작성할 때 비즈니스 로직에만 집중할 수 있다는 것입니다.

각 메서드에 대해 java.util.function을 사용합니다.컨슈머 오브젝트(consumer object)를 주장하기 때문에 언제든지 재사용할 수 있는 별도의 장소에 당사의 비즈니스 로직을 두는도움이 됩니다.

아래 토막을 봐주세요.

  • 여기서는 Consumer Class에서 Accept 클래스 메서드를 덮어쓰는 새로운 클래스를 만들었습니다.이 클래스에서 More than Repeating..!!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    

언급URL : https://stackoverflow.com/questions/16635398/java-8-iterable-foreach-vs-foreach-loop

반응형