Java final
A palavra final quer dizer "definitivo, não muda mais". Ela pode aparecer em cinco lugares de um programa Java, e em cada um ela trava uma coisa diferente: o valor de uma variável, um parâmetro, um atributo, um método ou uma classe inteira. Nesta aula você vai ver os cinco, um de cada vez, sempre com o erro que o javac mostra quando alguém tenta mexer no que é final.
- Como uma variável
finalrecebe um valor uma única vez. - Para que serve um parâmetro
final. - Como funciona um atributo
finale por que ele deve receber valor no construtor. - O que é um método
final(não pode ser sobrescrito) e uma classefinal(não pode ser herdada, comoString). - A diferença entre
finale imutável: por que uma listafinalainda pode ganhar e perder itens.
Você já viu final em Constantes, no Nível 1, e o static final no fim da aula anterior, static. Aqui o assunto é o final completo.
A ideia: escrito à caneta
Imagine uma ficha de matrícula. Alguns campos a Ana preenche a lápis: o telefone, o endereço, o curso; se mudarem, ela apaga e escreve de novo. Outros ela preenche à caneta: a data de nascimento, o CPF. Uma vez escritos, ficam.
Em Java, tudo é "a lápis" por padrão. A palavra final transforma em "caneta": dá para escrever uma vez, e qualquer tentativa de escrever de novo é recusada pelo compilador, antes de o programa rodar. O nome vem do inglês final, "último": aquele é o valor final.
| Onde aparece | O que o final impede |
|---|---|
| Variável local | Receber outro valor |
| Parâmetro de método | Receber outro valor dentro do método |
| Atributo | Mudar depois que o objeto foi criado |
| Método | Ser sobrescrito numa subclasse |
| Classe | Ser herdada (ter subclasses) |
1. Variáveis final
Uma variável comum pode mudar quantas vezes você quiser. Com final na frente, ela recebe um valor e fica com ele:
public class Main {
public static void main(String[] args) {
final int diasNaSemana = 7;
System.out.println(diasNaSemana);
diasNaSemana = 8;
}
}
- final
- "Esta variável recebe valor uma vez só."
- int diasNaSemana = 7;
- Uma declaração normal, com o valor inicial.
- diasNaSemana = 8;
- A segunda atribuição: é aqui que o compilador reclama.
$ javac Main.java
Main.java:5: error: cannot assign a value to final variable diasNaSemana
diasNaSemana = 8;
^
1 error
Cannot assign a value to final variable = "não é possível atribuir um valor à variável final". O erro aparece na compilação: o programa nem chega a rodar. Essa é a grande vantagem do final. Se em algum lugar do código alguém (inclusive você, daqui a um mês) tentar mudar um valor que não deveria mudar, o javac avisa na hora.
Valor dado depois, mas uma vez só
Uma variável final não precisa receber o valor na mesma linha. Ela pode ser declarada vazia e receber o valor depois, desde que seja exatamente uma vez em cada caminho do programa. Isso é útil com if/else:
public class Main {
public static void main(String[] args) {
int nota = 8;
final String situacao;
if (nota >= 6) {
situacao = "aprovada";
} else {
situacao = "reprovada";
}
System.out.println("Ana está " + situacao);
}
}
$ java Main.java Ana está aprovada
Em cada caminho (o do if e o do else), situacao recebe valor uma vez. O compilador confere isso. Se você errar para mais ou para menos, ele mostra:
$ javac Main.java
Main.java:5: error: variable situacao might already have been assigned
situacao = "reprovada";
^
Main.java:7: error: variable media might not have been initialized
System.out.println(media);
^
2 errors
- might already have been assigned = "pode já ter recebido valor": a linha 5 é a segunda atribuição de
situacao. - might not have been initialized = "pode não ter sido inicializada": a variável
final double media;foi usada sem nunca receber valor (esse erro existe para qualquer variável local; veja Variáveis).
final numa variável comum?
Para deixar uma intenção clara: "este valor, depois de calculado, não muda". Quem lê o código sabe que não precisa procurar outra atribuição mais abaixo. Muita gente marca como final toda variável que não muda. Não é obrigatório: é um hábito de clareza.
Missão: provoque o erro! O programa abaixo funciona. Coloque final na declaração de preco e rode: o javac deve recusar a linha do aumento com cannot assign a value to final variable preco. Repare que o erro aponta a linha 5, e não a 3.
public class Main {
public static void main(String[] args) {
double preco = 4.50;
System.out.println("Pão: R$ " + preco);
preco = preco + 0.50;
System.out.println("Pão: R$ " + preco);
}
}
public class Main {
public static void main(String[] args) {
final double preco = 4.50;
System.out.println("Pão: R$ " + preco);
preco = preco + 0.50;
System.out.println("Pão: R$ " + preco);
}
}
2. Parâmetros final
Um parâmetro é uma variável local que recebe o valor na chamada do método. Por isso, ele também pode ser final: dentro do método, ninguém pode trocar o valor que chegou.
public class Main {
static double comDesconto(final double preco, final double percentual) {
preco = preco - preco * percentual / 100;
return preco;
}
public static void main(String[] args) {
System.out.println(comDesconto(50, 10));
}
}
$ javac Main.java
Main.java:3: error: final parameter preco may not be assigned
preco = preco - preco * percentual / 100;
^
1 error
Final parameter preco may not be assigned = "o parâmetro final preco não pode receber valor". Reaproveitar o parâmetro para guardar outra coisa (aqui, o preço já com desconto) confunde quem lê: no meio do método, preco não é mais o preço original. O conserto é criar uma variável nova:
static double comDesconto(final double preco, final double percentual) {
double resultado = preco - preco * percentual / 100;
return resultado;
}
Com isso, comDesconto(50, 10) devolve 45.0. Parâmetros final são pouco comuns no dia a dia (muita gente acha que poluem a assinatura). Você vai encontrá-los em empresas que adotam a regra "nunca reatribua um parâmetro" e em código antigo que usava classes anônimas (veja Classes Internas e Anônimas).
3. Atributos final
Aqui o final fica mais interessante. Uma pessoa pode mudar de nome (casamento, por exemplo), mas não muda de CPF. A Ana modela isso assim:
public class Main {
public static void main(String[] args) {
Pessoa ana = new Pessoa("Ana", "123.456.789-00");
System.out.println(ana.nome + " - " + ana.cpf);
ana.nome = "Ana Souza";
System.out.println(ana.nome + " - " + ana.cpf);
}
}
class Pessoa {
String nome;
final String cpf;
Pessoa(String nome, String cpf) {
this.nome = nome;
this.cpf = cpf;
}
}
- String nome;
- Atributo comum: pode mudar a qualquer momento.
- final String cpf;
- Atributo
final, declarado sem valor. Cada pessoa terá o seu CPF, então o valor não pode estar aqui. - this.cpf = cpf;
- No construtor, o atributo recebe o valor. Essa é a única atribuição permitida (veja this).
$ java Main.java Ana - 123.456.789-00 Ana Souza - 123.456.789-00 « o nome mudou; o CPF continua o mesmo
Repare: o atributo final não tem um valor igual para todos (isso seria uma constante, static final). Cada objeto tem o seu CPF, decidido no momento em que é criado e travado a partir daí.
A regra: todo construtor tem que dar o valor
Um atributo final sem valor na declaração precisa receber valor em todo construtor. Se algum construtor esquecer, o objeto nasceria com um CPF que nunca poderia ser preenchido. O compilador não deixa:
public class Main {
public static void main(String[] args) {
Pessoa ana = new Pessoa("Ana");
}
}
class Pessoa {
String nome;
final String cpf;
Pessoa(String nome) {
this.nome = nome;
}
}
$ javac Main.java
Main.java:13: error: variable cpf might not have been initialized
}
^
1 error
O erro aponta a linha 13: a chave que fecha o construtor. É ali que o construtor termina sem ter dado valor ao cpf. Se a classe não tiver construtor nenhum, a mensagem muda um pouco:
$ javac Main.java
Main.java:8: error: variable cpf not initialized in the default constructor
final String cpf;
^
1 error
Not initialized in the default constructor = "não inicializada no construtor padrão", aquele construtor vazio que o Java cria quando você não escreve nenhum (veja Construtores).
E, claro, mudar o atributo depois de criado também é recusado, venha a tentativa de onde vier:
$ javac Main.java
Main.java:4: error: cannot assign a value to final variable cpf
ana.cpf = "000.000.000-00";
^
1 error
Onde o atributo final pode receber valor | Exemplo |
|---|---|
| Na declaração (mesmo valor para todos os objetos) | final int maxLivros = 3; |
| Em todo construtor (um valor por objeto) | this.cpf = cpf; |
Num bloco de inicialização { … } (raro) | { criadoEm = System.currentTimeMillis(); } |
Em qualquer outro lugar (métodos, main, setters) | Erro de compilação |
final não tem setter
Na aula de Encapsulamento você criou getNome() e setNome(). Para um atributo final, só existe o get: um setCpf não compilaria. Isso é bom: a própria classe documenta que o CPF não muda.
Missão: a classe Livro tem o atributo final String isbn, mas o construtor esqueceu dele, e o código não compila. Rode para ver o erro. Depois faça o construtor receber também o ISBN (Livro(String titulo, String isbn)) e crie o livro com new Livro("Dom Casmurro", "9788535910663"). A saída deve ser Dom Casmurro (ISBN 9788535910663).
public class Main {
public static void main(String[] args) {
Livro livro = new Livro("Dom Casmurro");
System.out.println(livro.titulo + " (ISBN " + livro.isbn + ")");
}
}
class Livro {
String titulo;
final String isbn;
Livro(String titulo) {
this.titulo = titulo;
}
}
public class Main {
public static void main(String[] args) {
Livro livro = new Livro("Dom Casmurro", "9788535910663");
System.out.println(livro.titulo + " (ISBN " + livro.isbn + ")");
}
}
class Livro {
String titulo;
final String isbn;
Livro(String titulo, String isbn) {
this.titulo = titulo;
this.isbn = isbn;
}
}
4. Métodos final: não podem ser sobrescritos
Os dois próximos usos dependem de herança, que é o assunto do grupo de aulas que começa em Herança. Em poucas palavras: uma classe pode "herdar" de outra (class ContaEspecial extends Conta) e sobrescrever um método, isto é, escrever a sua própria versão dele (veja Sobrescrita).
Às vezes isso é perigoso. O banco da Ana tem uma regra: nunca sacar mais do que o saldo. Se uma subclasse pudesse trocar o método sacar, poderia furar a regra. Com final, o método fica travado:
public class Main {
public static void main(String[] args) {
Conta c = new ContaEspecial();
c.sacar(50);
}
}
class Conta {
double saldo = 100;
final void sacar(double valor) {
if (valor <= saldo) {
saldo = saldo - valor;
}
System.out.println("Saldo: " + saldo);
}
}
class ContaEspecial extends Conta {
void sacar(double valor) {
saldo = saldo - valor;
}
}
$ javac Main.java
Main.java:20: error: sacar(double) in ContaEspecial cannot override sacar(double) in Conta
void sacar(double valor) {
^
overridden method is final
1 error
Traduzindo: "sacar(double) em ContaEspecial não pode sobrescrever sacar(double) em Conta"; a linha de baixo dá o motivo: overridden method is final, "o método sobrescrito é final". A ContaEspecial continua herdando e podendo usar o sacar; ela só não pode trocá-lo. Se você apagar o método da ContaEspecial, o programa compila e mostra Saldo: 50.0.
5. Classes final: não podem ser herdadas
Um passo além: uma classe final não pode ter subclasses. Ninguém pode escrever extends com ela. O exemplo mais famoso é a própria String:
public class Main {
public static void main(String[] args) {
System.out.println("teste");
}
}
class MinhaString extends String {
}
$ javac Main.java
Main.java:7: error: cannot inherit from final String
class MinhaString extends String {
^
1 error
Cannot inherit from final String = "não é possível herdar da classe final String". Os criadores do Java declararam public final class String por segurança: o programa inteiro confia que um texto é um texto. Se fosse possível criar uma "String" falsa que muda sozinha, uma senha ou um caminho de arquivo poderiam ser trocados depois de conferidos. Outras classes final da biblioteca: Integer, Double e os outros wrappers, Math, System, LocalDate.
Para as suas classes, a sintaxe é a mesma:
public final class Cpf {
// ninguém pode escrever "extends Cpf"
}
Você também vai ver classes final nos records (todo record é final automaticamente) e uma alternativa mais flexível, as classes seladas, que dizem exatamente quem pode herdar.
final não é o mesmo que imutável
Esta é a parte que mais confunde, até programadores experientes. O final trava a variável, não o objeto para onde ela aponta.
Lembre-se: uma variável de um tipo objeto (como List, um array, um Aluno) guarda uma referência, um "endereço" do objeto. Com final, esse endereço não muda: a variável vai apontar sempre para o mesmo objeto. Mas o objeto em si pode mudar por dentro.
Pense numa caixa de compras etiquetada com caneta: "caixa da Ana". A etiqueta não muda (é sempre a mesma caixa), mas você pode colocar e tirar coisas de dentro dela.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
final List<String> frutas = new ArrayList<>();
frutas.add("maçã");
frutas.add("banana");
System.out.println(frutas);
frutas.remove("maçã");
frutas.add("uva");
System.out.println(frutas);
final int[] notas = {7, 8, 9};
notas[0] = 10;
System.out.println(notas[0]);
}
}
$ java Main.java [maçã, banana] [banana, uva] « a lista final mudou por dentro 10 « o array final também
Nenhum erro: add, remove e notas[0] = 10 mexem no conteúdo, não na variável. (A ArrayList tem uma aula própria: ArrayList.) O que o final proíbe é trocar a caixa inteira:
$ javac Main.java
Main.java:7: error: cannot assign a value to final variable frutas
frutas = new ArrayList<>();
^
1 error
Com final List<String> frutas = new ArrayList<>(); | Pode? |
|---|---|
frutas.add("uva") | Sim: muda o conteúdo |
frutas.remove("uva"), frutas.clear() | Sim: muda o conteúdo |
frutas = new ArrayList<>() | Não: troca a referência |
frutas = null | Não: troca a referência |
E se eu quiser uma lista que não muda nunca?
Aí você precisa de um objeto imutável, um objeto que não tem nenhum método capaz de alterá-lo. A String é imutável (todo método dela devolve uma String nova). Para listas, existe List.of(…), que cria uma lista que recusa mudanças. Mas repare: essa recusa só acontece quando o programa roda, como uma exceção:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> frutas = List.of("maçã", "banana");
System.out.println(frutas);
frutas.add("uva");
}
}
$ java Main.java [maçã, banana] Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159) at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:164) at Main.main(Main.java:7)
UnsupportedOperationException = "exceção de operação não suportada": a lista não aceita add. A última linha do rastro (Main.java:7) mostra a linha do seu código que causou o problema (exceções são o assunto de O que são Exceções).
final | Imutável | |
|---|---|---|
| O que trava | A variável (para onde ela aponta) | O objeto (o conteúdo) |
| Quem garante | O compilador, antes de rodar | A classe do objeto, ao rodar |
| Exemplo | final List<String> l = new ArrayList<>(); | List.of("a", "b"), String, records |
| Juntos | final List<String> l = List.of("a", "b");: nem a variável nem a lista mudam | |
Para criar as suas próprias classes imutáveis, junte o que você viu: atributos private final, nenhum setter e, de preferência, a classe final. Ou use um record, que faz tudo isso numa linha.
Um parente: "efetivamente final"
Você vai encontrar a expressão effectively final ("efetivamente final") em mensagens de erro. É uma variável que não tem a palavra final, mas nunca muda depois de receber o valor; para o compilador, ela se comporta como se tivesse. Isso importa em lambdas e classes anônimas, que só podem usar variáveis locais finais ou efetivamente finais. Por enquanto, basta saber que o termo existe.
Quando dá errado
$ javac Main.java
Main.java:5: error: cannot assign a value to final variable diasNaSemana
diasNaSemana = 8;
^
1 error
O que aconteceu: uma variável ou atributo final recebeu um segundo valor.
Como resolver: pergunte-se se o valor deveria mesmo mudar. Se sim, tire o final. Se não, guarde o novo valor numa variável nova (double precoComAumento = preco + 0.50;).
$ javac Main.java
Main.java:13: error: variable cpf might not have been initialized
}
^
1 error
O que aconteceu: um construtor terminou sem dar valor a um atributo final. Se a classe tem vários construtores, o erro aparece no que esqueceu.
Como resolver: acrescente this.cpf = …; nesse construtor (ou faça-o chamar outro construtor com this(…)).
$ javac Main.java
Main.java:20: error: sacar(double) in ContaEspecial cannot override sacar(double) in Conta
void sacar(double valor) {
^
overridden method is final
1 error
O que aconteceu: a subclasse tentou escrever a própria versão de um método final da superclasse.
Como resolver: dê outro nome ao método da subclasse, ou use o método herdado como está. Tirar o final da superclasse só se você for o dono dela e tiver certeza de que a troca é segura.
$ javac Main.java
Main.java:7: error: cannot inherit from final String
class MinhaString extends String {
^
1 error
O que aconteceu: o extends apontou para uma classe final.
Como resolver: em vez de herdar, guarde um objeto da classe como atributo (class Cpf { private final String numero; … }). Isso se chama composição, e é a saída padrão quando não dá para herdar.
O que aconteceu: nada de errado com o Java: final trava a variável, não o conteúdo.
Como resolver: se o conteúdo também não deve mudar, crie a lista com List.of(…) ou entregue uma cópia protegida com List.copyOf(lista).
Referência
| Escrita | Significado | Erro se desrespeitar |
|---|---|---|
final int x = 5; | Variável com valor único | cannot assign a value to final variable x |
final String s; + uma atribuição | Valor dado depois, uma vez | variable s might already have been assigned |
void f(final int p) | Parâmetro que não muda | final parameter p may not be assigned |
final String cpf; (atributo) | Valor por objeto, dado no construtor | variable cpf might not have been initialized |
static final int MAX = 10; | Constante da classe | cannot assign a value to final variable MAX |
final void sacar(…) | Método que não pode ser sobrescrito | … cannot override … / overridden method is final |
final class Cpf | Classe que não pode ser herdada | cannot inherit from final Cpf |
Exercícios
1. Valor único
Complete para a variável pi nunca poder mudar:
2. O construtor completo
A classe Aluno tem final int matricula;. Complete a linha do construtor Aluno(int matricula) que dá o valor ao atributo:
3. A classe que não tem filhos
Complete para ninguém poder escrever extends Moeda:
4. Com final List<String> nomes = new ArrayList<>();, qual linha não compila?
5. Por que class Texto extends String { } não compila?
6. O que um método final impede?
Missão final: a lista de compras da Ana é final. Depois de ir ao mercado, ela quis "esvaziar" a lista trocando-a por uma nova, e o código não compila. Rode para ver o erro. Conserte sem tirar o final: use compras.clear() para esvaziar a lista e depois adicione "café". A saída final deve ser [café].
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
final List<String> compras = new ArrayList<>();
compras.add("arroz");
compras.add("feijão");
System.out.println(compras);
compras = new ArrayList<>();
System.out.println(compras);
}
}
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
final List<String> compras = new ArrayList<>();
compras.add("arroz");
compras.add("feijão");
System.out.println(compras);
compras.clear();
compras.add("café");
System.out.println(compras);
}
}
Resumo
final= "recebe valor uma vez". O compilador garante, antes de o programa rodar.- Variável local e parâmetro
final: não podem receber outro valor (cannot assign a value to final variable,final parameter … may not be assigned). - Atributo
final: recebe valor na declaração ou em todo construtor; depois, nunca mais. Não tem setter. - Método
final: subclasses não podem sobrescrevê-lo (overridden method is final). - Classe
final: não pode ser herdada (cannot inherit from final String). finaltrava a variável, não o objeto: uma listafinalainda aceitaadd. Para conteúdo que não muda, use objetos imutáveis (List.of,String, records).