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.

Você vai aprender
  • Como uma variável final recebe um valor uma única vez.
  • Para que serve um parâmetro final.
  • Como funciona um atributo final e por que ele deve receber valor no construtor.
  • O que é um método final (não pode ser sobrescrito) e uma classe final (não pode ser herdada, como String).
  • A diferença entre final e imutável: por que uma lista final ainda 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 apareceO que o final impede
Variável localReceber outro valor
Parâmetro de métodoReceber outro valor dentro do método
AtributoMudar depois que o objeto foi criado
MétodoSer sobrescrito numa subclasse
ClasseSer 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:

Este código tem um erro de propósito
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).
Por que usar 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.

Este código tem um erro de propósito
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:

Este código tem um erro de propósito
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 valorExemplo
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
Atributo 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:

Este código tem um erro de propósito
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:

Este código tem um erro de propósito
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 = nullNã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).

finalImutável
O que travaA variável (para onde ela aponta)O objeto (o conteúdo)
Quem garanteO compilador, antes de rodarA classe do objeto, ao rodar
Exemplofinal List<String> l = new ArrayList<>();List.of("a", "b"), String, records
Juntosfinal 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

"cannot assign a value to final variable …"
$ 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;).

"variable … might not have been initialized" na chave do construtor
$ 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(…)).

"overridden method is final"
$ 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.

"cannot inherit from final …"
$ 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.

A lista "final" mudou

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

EscritaSignificadoErro se desrespeitar
final int x = 5;Variável com valor únicocannot assign a value to final variable x
final String s; + uma atribuiçãoValor dado depois, uma vezvariable s might already have been assigned
void f(final int p)Parâmetro que não mudafinal parameter p may not be assigned
final String cpf; (atributo)Valor por objeto, dado no construtorvariable cpf might not have been initialized
static final int MAX = 10;Constante da classecannot 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 CpfClasse que não pode ser herdadacannot inherit from final Cpf

Exercícios

1. Valor único

Complete para a variável pi nunca poder mudar:

double pi = 3.14159;

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:

public 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).
  • final trava a variável, não o objeto: uma lista final ainda aceita add. Para conteúdo que não muda, use objetos imutáveis (List.of, String, records).