람다 내부에서 지역 변수 수정


115

에서 지역 변수 수정 forEach 컴파일 오류가 발생합니다.

표준

    int ordinal = 0;
    for (Example s : list) {
        s.setOrdinal(ordinal);
        ordinal++;
    }

Lambda 사용

    int ordinal = 0;
    list.forEach(s -> {
        s.setOrdinal(ordinal);
        ordinal++;
    });

이 문제를 해결하는 방법을 아십니까?


8
람다가 본질적으로 익명의 내부 클래스에 대한 통사론 적 설탕이라는 것을 고려할 때, 내 직감은 최종이 아닌 지역 변수를 캡처하는 것이 불가능하다는 것입니다. 그래도 나는 틀렸다는 것을 증명하고 싶습니다.
Sinkingpoint

2
람다 식에 사용되는 변수는 사실상 최종적이어야합니다. 과도하지만 원자 정수를 사용할 수 있으므로 람다식이 실제로 필요하지 않습니다. for 루프를 고수하십시오.
Alexis C.

3
변수는 사실상 최종적 이어야합니다 . 이것을보십시오 : 왜 지역 변수 캡처에 제한
Jesper


2
@Quirliom 그들은 익명의 클래스에 대한 구문 설탕이 아닙니다. Lambdas는 내부적으로 메서드 핸들을 사용합니다
Dioxin 2015 년

답변:


177

래퍼 사용

어떤 종류의 래퍼도 좋습니다.

자바 8 이상 , 사용 어느 쪽 AtomicInteger:

AtomicInteger ordinal = new AtomicInteger(0);
list.forEach(s -> {
  s.setOrdinal(ordinal.getAndIncrement());
});

... 또는 배열 :

int[] ordinal = { 0 };
list.forEach(s -> {
  s.setOrdinal(ordinal[0]++);
});

10 + 자바 :

var wrapper = new Object(){ int ordinal = 0; };
list.forEach(s -> {
  s.setOrdinal(wrapper.ordinal++);
});

노트 : 병렬 스트림을 사용하는 경우 매우주의하십시오. 예상 한 결과를 얻지 못할 수도 있습니다. Stuart와 같은 다른 솔루션은 이러한 경우에 더 적합 할 수 있습니다.

이외의 유형 int

물론 이것은 int. 래핑 유형을 AtomicReference또는 해당 유형의 배열 로 변경하기 만하면 됩니다. 예를 들어를 사용하는 경우 String다음을 수행하십시오.

AtomicReference<String> value = new AtomicReference<>();
list.forEach(s -> {
  value.set("blah");
});

배열 사용 :

String[] value = { null };
list.forEach(s-> {
  value[0] = "blah";
});

또는 Java 10 이상 :

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});

왜 다음과 같은 배열을 사용할 수 int[] ordinal = { 0 };있습니까? 설명해 주시겠습니까? 감사합니다
mrbela 19 년

@mrbela 정확히 무엇을 이해하지 못합니까? 특정 부분을 명확히 할 수 있을까요?
Olivier Grégoire

1
@mrbela 배열은 참조로 전달됩니다. 배열을 전달할 때 실제로 해당 배열의 메모리 주소를 전달합니다. 정수와 같은 기본 유형은 값으로 전송됩니다. 이는 값의 사본이 전달됨을 의미합니다. 값에 의한 전달 원래 값과 메소드로 전송 된 사본 사이에는 관계가 없습니다. 사본을 조작하는 것은 원본에 아무 일도하지 않습니다. 참조로 전달한다는 것은 원래 값과 메서드로 전송 된 값이 동일한 값임을 의미합니다. 메서드에서이를 조작하면 메서드 외부의 값이 변경됩니다.
DetectivePikachu

@Olivier Grégoire는 정말 내 가죽을 구했습니다. "var"와 함께 Java 10 솔루션을 사용했습니다. 어떻게 작동하는지 설명해 주시겠습니까? 나는 모든 곳에서 Google을 검색했으며이 특정 용도로 찾은 유일한 장소입니다. 내 생각에 람다는 범위 밖에서 (효과적으로) 최종 객체 만 허용하기 때문에 "var"객체는 기본적으로 최종 객체 만 범위 밖에서 참조 될 것이라고 가정하기 때문에 컴파일러가 최종 객체라고 추론하도록 속이는 것입니다. 잘 모르겠습니다. 그게 최선의 추측입니다. 설명을 제공하고 싶다면 내 코드가 작동하는 이유를 알고
싶기

1
@oaker 이것은 Java가 다음과 같이 MyClass $ 1 클래스를 생성하기 때문에 작동합니다. class MyClass$1 { int value; }일반적인 클래스에서 이것은라는 이름의 package-private 변수로 클래스를 생성한다는 것을 의미합니다 value. 이것은 동일하지만 클래스는 실제로 컴파일러 나 JVM이 아닌 우리에게 익명입니다. Java는 코드를 마치 MyClass$1 wrapper = new MyClass$1();. 그리고 익명의 클래스는 또 다른 클래스가됩니다. 결국, 우리는 그것을 읽을 수 있도록 그 위에 구문 적 설탕을 추가했습니다. 또한 클래스는 package-private 필드가있는 내부 클래스입니다. 해당 필드를 사용할 수 있습니다.
Olivier Grégoire

14

이것은 XY 문제에 상당히 가깝습니다. . 즉, 질문은 본질적으로 람다에서 캡처 된 지역 변수를 변경하는 방법입니다. 그러나 실제 작업은 목록의 요소에 번호를 매기는 방법입니다.

내 경험상 80 % 이상의 시간에 람다 내에서 캡처 된 로컬을 변경하는 방법에 대한 질문이 있습니다. 더 나은 진행 방법이 있습니다. 일반적으로 여기에는 감소가 포함되지만이 경우 목록 인덱스에서 스트림을 실행하는 기술이 잘 적용됩니다.

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));

3
좋은 해결책하지만 경우 listA는 RandomAccess리스트
ZhekaKozlov

k 반복마다 진행 메시지 문제를 전용 카운터없이 실제로 해결할 수 있는지 궁금합니다. 즉,이 경우 : stream.forEach (e-> {doSomething (e); if (++ ctr % 1000 = = 0) log.info ( "I 've processing {} elements", ctr);} 더 실용적인 방법은 보이지 않습니다 (감소는 할 수 있지만 특히 병렬 스트림에서는 더 장황 할 것입니다).
zakmck

14

외부에서 람다로 값을 전달하고 가져 오지 않으면 람다 대신 일반 익명 클래스를 사용하여 수행 할 수 있습니다.

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});

1
... 실제로 결과를 읽어야한다면 어떻게해야합니까? 결과는 최상위 수준의 코드에 표시되지 않습니다.
Luke Usherwood

@LukeUsherwood : 당신이 맞아요. 이것은 외부에서 람다로 데이터를 전달해야하는 경우에만 해당됩니다. 그것을 꺼내야하는 경우, 람다가 가변 객체 (예 : 배열 또는 최종 공개 필드가없는 객체)에 대한 참조를 캡처하고이를 객체에 설정하여 데이터를 전달하도록해야합니다.
newacct


3

Java 10을 사용 var하는 경우 다음을 사용할 수 있습니다 .

var ordinal = new Object() { int value; };
list.forEach(s -> {
    s.setOrdinal(ordinal.value);
    ordinal.value++;
});

3

AtomicInteger(또는 값을 저장할 수있는 다른 객체) 의 대안 은 배열을 사용하는 것입니다.

final int ordinal[] = new int[] { 0 };
list.forEach ( s -> s.setOrdinal ( ordinal[ 0 ]++ ) );

그러나 Stuart의 답변을 참조하십시오 . 귀하의 사건을 처리하는 더 좋은 방법이있을 수 있습니다.


2

나는 그것이 오래된 질문이라는 것을 알고 있지만, 외부 변수가 최종적이어야한다는 사실을 고려하여 해결 방법에 익숙하다면 간단히 다음과 같이 할 수 있습니다.

final int[] ordinal = new int[1];
list.forEach(s -> {
    s.setOrdinal(ordinal[0]);
    ordinal[0]++;
});

가장 우아하거나 가장 정확하지 않을 수도 있지만 그렇게 될 것입니다.


1

컴파일러를 해결하기 위해 래핑 할 수 있지만 람다의 부작용은 권장되지 않습니다.

javadoc 을 인용하려면

스트림 작업에 대한 동작 매개 변수의 부작용은 종종 무의식적으로 상태 비 저장 요구 사항을 위반할 수 있으므로 권장되지 않습니다. forEach () 및 peek ()와 같은 소수의 스트림 작업은 side를 통해서만 작동 할 수 있습니다. -효과; 조심해서 사용해야합니다.


0

나는 약간 다른 문제가 있었다. forEach에서 지역 변수를 증가시키는 대신 개체를 지역 변수에 할당해야했습니다.

반복하려는 목록 (countryList)과 해당 목록 (foundCountry)에서 얻고 자하는 출력을 모두 래핑하는 비공개 내부 도메인 클래스를 정의하여이 문제를 해결했습니다. 그런 다음 Java 8 "forEach"를 사용하여 목록 필드를 반복하고 원하는 객체를 찾으면 해당 객체를 출력 필드에 할당합니다. 따라서 이것은 지역 변수 자체를 변경하지 않고 지역 변수의 필드에 값을 할당합니다. 나는 지역 변수 자체가 변경되지 않았기 때문에 컴파일러가 불평하지 않는다고 생각합니다. 그런 다음 목록 외부의 출력 필드에서 캡처 한 값을 사용할 수 있습니다.

도메인 개체 :

public class Country {

    private int id;
    private String countryName;

    public Country(int id, String countryName){
        this.id = id;
        this.countryName = countryName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountryName() {
        return countryName;
    }

    public void setCountryName(String countryName) {
        this.countryName = countryName;
    }
}

래퍼 개체 :

private class CountryFound{
    private final List<Country> countryList;
    private Country foundCountry;
    public CountryFound(List<Country> countryList, Country foundCountry){
        this.countryList = countryList;
        this.foundCountry = foundCountry;
    }
    public List<Country> getCountryList() {
        return countryList;
    }
    public void setCountryList(List<Country> countryList) {
        this.countryList = countryList;
    }
    public Country getFoundCountry() {
        return foundCountry;
    }
    public void setFoundCountry(Country foundCountry) {
        this.foundCountry = foundCountry;
    }
}

반복 작업 :

int id = 5;
CountryFound countryFound = new CountryFound(countryList, null);
countryFound.getCountryList().forEach(c -> {
    if(c.getId() == id){
        countryFound.setFoundCountry(c);
    }
});
System.out.println("Country found: " + countryFound.getFoundCountry().getCountryName());

래퍼 클래스 메서드 "setCountryList ()"를 제거하고 "countryList"필드를 최종적으로 만들 수 있지만 이러한 세부 정보를 그대로 유지하면서 컴파일 오류가 발생하지 않았습니다.


0

보다 일반적인 솔루션을 얻으려면 일반 Wrapper 클래스를 작성할 수 있습니다.

public static class Wrapper<T> {
    public T obj;
    public Wrapper(T obj) { this.obj = obj; }
}
...
Wrapper<Integer> w = new Wrapper<>(0);
this.forEach(s -> {
    s.setOrdinal(w.obj);
    w.obj++;
});

(이것은 Almir Campos가 제공 한 솔루션의 변형입니다).

특정 경우에 이것은 귀하의 목적 Integer보다 나쁘기 때문에 좋은 해결책이 아닙니다. int어쨌든이 해결책은 더 일반적이라고 생각합니다.


0

예, 다른 답변에 표시된 방식대로 람다 내부에서 지역 변수를 수정할 수 있지만 그렇게해서는 안됩니다. Lambda는 프로그래밍의 기능적 스타일을위한 것이며 이는 다음을 의미합니다. 부작용이 없습니다. 당신이 원하는 것은 나쁜 스타일로 간주됩니다. 병렬 스트림의 경우에도 위험합니다.

부작용이없는 솔루션을 찾거나 기존 for 루프를 사용해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.