A variável usada na expressão lambda deve ser final ou efetivamente final

134

A variável usada na expressão lambda deve ser final ou efetivamente final

Quando tento usá- calTzlo, está mostrando esse erro.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
user3610470
fonte
5
Você não pode modificar a calTzpartir do lambda.
22316 Elliott Frisch
2
Eu assumi que essa era uma daquelas coisas que não foram concluídas a tempo para o Java 8. Mas o Java 8 foi 2014. Scala e Kotlin permitem isso há anos, então é obviamente possível. O Java está planejando eliminar essa restrição estranha?
GlenPeterson
5
Aqui está o link atualizado para o comentário de @MSDousti.
geisterfurz007
Eu acho que você poderia usar os Completable Futures como uma solução alternativa.
Kraulain
Uma coisa importante que observei - você pode usar variáveis ​​estáticas em vez de variáveis ​​normais (isso torna efetivamente final, eu acho)
kaushalpranav

Respostas:

68

Uma finalvariável significa que ela pode ser instanciada apenas uma vez. em Java, você não pode usar variáveis ​​não finais no lambda, bem como em classes internas anônimas.

Você pode refatorar seu código com o antigo loop for-each:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

Mesmo que eu não entenda algumas partes deste código:

  • você chama um v.getTimeZoneId();sem usar seu valor de retorno
  • com a atribuição, calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());você não modifica o originalmente aprovado calTze não o usa neste método
  • Você sempre retorna null, por que não define voidcomo tipo de retorno?

Espero que essas dicas também ajudem você a melhorar.

Francesco Pitzalis
fonte
podemos usar variáveis ​​estáticas não finais
Narendra Jaggi
92

Embora outras respostas provem o requisito, elas não explicam por que o requisito existe.

O JLS menciona por que, em § 15.27.2 :

A restrição para variáveis ​​efetivamente finais proíbe o acesso a variáveis ​​locais que mudam dinamicamente, cuja captura provavelmente introduziria problemas de simultaneidade.

Para diminuir o risco de bugs, eles decidiram garantir que as variáveis ​​capturadas nunca fossem mutadas.

Dioxina
fonte
10
Boa resposta +1, e estou surpreso com a pouca cobertura que parece ter o motivo para efetivamente final. Nota: Uma variável local só pode ser capturada por um lambda se também estiver definitivamente definida antes do corpo do lambda. Ambos os requisitos parecem garantir que o acesso à variável local seja seguro para threads.
Tim Biegeleisen 14/09
2
Alguma idéia de por que isso é restrito apenas a variáveis ​​locais e não a membros da classe? Encontro-me muitas vezes contornar o problema, declarando meu variável como um membro da classe ...
David Refaeli
4
Os membros da classe @DavidRefaeli são cobertos / afetados pelo modelo de memória que, se seguido, produzirá resultados previsíveis quando compartilhados. Variáveis ​​locais não são, como mencionado em §17.4.1
Dioxina
Este é um hack bobo, que deve ser removido. O compilador deve avisar sobre o potencial acesso a variáveis ​​entre threads, mas deve permitir. Ou, deve ser inteligente o suficiente para saber se o seu lambda está sendo executado no mesmo encadeamento ou paralelamente etc. Essa é uma limitação boba, que me deixa triste. E como outros já mencionaram, os problemas não existem em, por exemplo, C #.
Josh M.
@JoshM. O C # também permite criar tipos de valor mutável , que as pessoas recomendam evitar para evitar problemas. Em vez de ter esses princípios, o Java decidiu evitá-lo completamente. Reduz o erro do usuário, ao preço da flexibilidade. Não concordo com esta restrição, mas é justificável. A contabilização do paralelismo exigiria algum trabalho extra no final do compilador, e é provavelmente por isso que a rota de " aviso de acesso cruzado " não foi tomada. Um desenvolvedor trabalhando nas especificações provavelmente seria nossa única confirmação para isso.
Dioxin
57

De um lambda, você não pode obter uma referência a nada que não seja final. Você precisa declarar um wrapper final de fora do lamda para manter sua variável.

Eu adicionei o objeto 'referência' final como este wrapper.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   
DMozzy
fonte
Eu estava pensando na mesma abordagem ou em uma abordagem semelhante - mas gostaria de receber aconselhamento / feedback sobre esta resposta?
YoYo
4
Este código perde uma inicial reference.set(calTz);ou a referência deve ser criada usando new AtomicReference<>(calTz), caso contrário, o TimeZone não nulo fornecido como parâmetro será perdido.
Julien Kronegg
7
Essa deve ser a primeira resposta. Um AtomicReference (ou classe Atomic___ similar) trabalha em torno dessa limitação com segurança em todas as circunstâncias possíveis.
precisa saber é o seguinte
1
Concordado, esta deve ser a resposta aceita. As outras respostas fornecem informações úteis sobre como retornar a um modelo de programação não-funcional e sobre o motivo de isso ter sido feito, mas na verdade não informam como solucionar o problema!
Jonathan Benn
2
@GlenPeterson e também é uma péssima decisão, não apenas muito mais lenta dessa maneira, mas você também está ignorando a propriedade de efeitos colaterais exigida pela documentação.
Eugene
41

O Java 8 tem um novo conceito chamado variável "Efetivamente final". Isso significa que uma variável local não final, cujo valor nunca muda após a inicialização, é chamada "Efetivamente Final".

Esse conceito foi introduzido porque, antes do Java 8 , não era possível usar uma variável local não final em uma classe anônima . Se você quiser ter acesso a uma variável local na classe anônima , precisará finalizá-la.

Quando o lambda foi introduzido, essa restrição foi facilitada. Portanto, a necessidade de tornar a variável local final se ela não for alterada, uma vez que é inicializada como lambda em si, não passa de uma classe anônima.

O Java 8 percebeu a dificuldade de declarar a variável local final toda vez que um desenvolvedor usou o lambda, introduziu esse conceito e tornou desnecessário finalizar as variáveis ​​locais. Portanto, se você vê que a regra para classes anônimas não mudou, é só que você não precisa escrever a finalpalavra-chave sempre que usar lambdas.

Eu encontrei uma boa explicação aqui

Dinesh Arora
fonte
A formatação de código deve ser usada apenas para código , não para termos técnicos em geral. effectively finalnão é código, é terminologia. Consulte Quando a formatação de código deve ser usada para texto sem código? no Meta Stack Overflow .
Charles Duffy
(Portanto, "a finalpalavra - chave" é uma palavra de código e correta para formatar dessa maneira, mas quando você usa "final" descritivamente, e não como código, é uma terminologia).
Charles Duffy
9

No seu exemplo, você pode substituir o forEachwith lamdba por um forloop simples e modificar qualquer variável livremente. Ou, provavelmente, refatorar seu código para que você não precise modificar nenhuma variável. No entanto, explicarei por completo o que significa o erro e como solucionar o problema.

Especificação da linguagem Java 8, §15.27.2 :

Qualquer variável local, parâmetro formal ou parâmetro de exceção usado, mas não declarado em uma expressão lambda, deve ser declarado final ou efetivamente final ( §4.12.4 ), ou ocorre um erro em tempo de compilação quando o uso é tentado.

Basicamente, você não pode modificar uma variável local ( calTzneste caso) de dentro de uma lambda (ou uma classe local / anônima). Para conseguir isso em Java, você deve usar um objeto mutável e modificá-lo (por meio de uma variável final) a partir do lambda. Um exemplo de um objeto mutável aqui seria uma matriz de um elemento:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}
Alexander Udalov
fonte
Outra maneira é usar o campo de um objeto. Por exemplo, resultado MyObj = novo MyObj (); ...; result.timeZone = ...; ....; retornar result.timezone; Observe, porém, que, conforme explicado acima, isso expõe você a problemas de segurança de threads. Veja stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

se não for necessário modificar a variável, uma solução geral para esse tipo de problema seria extrair a parte do código que usa lambda e usa a palavra-chave final no método-parâmetro.

robie2011
fonte
0

Uma variável usada na expressão lambda deve ser final ou efetivamente final, mas você pode atribuir um valor a uma matriz final de um elemento.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
Andreas Foteas
fonte
Isso é muito semelhante à sugestão de Alexander Udalov. Além disso, acho que essa abordagem depende de efeitos colaterais.
Scratte