Classi Final in Java

Edoardo Midali
Edoardo Midali

Le classi final rappresentano un meccanismo fondamentale in Java per prevenire l’ereditarietà e garantire l’immutabilità del design. Quando una classe è dichiarata final, non può essere estesa da nessun’altra classe, terminando definitivamente la catena di ereditarietà. Questa caratteristica offre vantaggi significativi in termini di sicurezza, performance e design intent.

Concetto e Motivazione

Il modificatore final applicato alle classi serve a proteggere l’integrità del design e a comunicare chiaramente che la classe è completa e non dovrebbe essere modificata attraverso l’ereditarietà. Questa decisione di design ha implicazioni profonde sulla struttura del codice e sulle ottimizzazioni possibili.

Ragioni per Rendere Final una Classe

Integrità del Design: Alcune classi sono progettate per funzionare in modo specifico e l’ereditarietà potrebbe compromettere questa integrità. Classi come String, Integer, e LocalDate sono final per garantire comportamenti prevedibili e consistenti.

Sicurezza: Prevenire l’override di metodi critici attraverso sottoclassi può essere essenziale per la sicurezza dell’applicazione, specialmente in contesti dove il codice client potrebbe non essere trusted.

Ottimizzazioni delle Performance: Il compilatore e la JVM possono applicare ottimizzazioni più aggressive quando sanno che una classe non verrà mai estesa.

Immutabilità: Molte classi final sono progettate per essere immutabili, e prevenire l’ereditarietà è spesso parte di questa strategia di design.

// Esempio di classe final per garantire immutabilità
public final class Coordinate {
    private final double latitudine;
    private final double longitudine;

    public Coordinate(double latitudine, double longitudine) {
        if (latitudine < -90 || latitudine > 90) {
            throw new IllegalArgumentException("Latitudine deve essere tra -90 e 90");
        }
        if (longitudine < -180 || longitudine > 180) {
            throw new IllegalArgumentException("Longitudine deve essere tra -180 e 180");
        }

        this.latitudine = latitudine;
        this.longitudine = longitudine;
    }

    public double getLatitudine() {
        return latitudine;
    }

    public double getLongitudine() {
        return longitudine;
    }

    public double distanzaDa(Coordinate altra) {
        // Implementazione della formula di Haversine
        double R = 6371; // Raggio della Terra in km
        double dLat = Math.toRadians(altra.latitudine - this.latitudine);
        double dLon = Math.toRadians(altra.longitudine - this.longitudine);

        double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                Math.cos(Math.toRadians(this.latitudine)) *
                Math.cos(Math.toRadians(altra.latitudine)) *
                Math.sin(dLon/2) * Math.sin(dLon/2);

        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Coordinate that = (Coordinate) obj;
        return Double.compare(that.latitudine, latitudine) == 0 &&
               Double.compare(that.longitudine, longitudine) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(latitudine, longitudine);
    }

    @Override
    public String toString() {
        return String.format("Coordinate{lat=%.6f, lon=%.6f}", latitudine, longitudine);
    }
}

Classi Final nelle Librerie Standard

Molte classi fondamentali di Java sono final, dimostrando l’importanza di questo pattern nelle API ben progettate.

String - Il Caso Paradigmatico

La classe String è final per ragioni di sicurezza, performance e design. La sua immutabilità combinata con il divieto di estensione garantisce comportamenti prevedibili in tutto l’ecosistema Java.

// Non è possibile estendere String
// public class MiaString extends String { } // Errore di compilazione

// Invece, si utilizzano pattern di composizione
public final class StringProcessor {
    private final String content;

    public StringProcessor(String content) {
        this.content = Objects.requireNonNull(content);
    }

    public StringProcessor normalize() {
        return new StringProcessor(content.trim().toLowerCase());
    }

    public StringProcessor removeSpaces() {
        return new StringProcessor(content.replaceAll("\\s+", ""));
    }

    public StringProcessor capitalize() {
        if (content.isEmpty()) return this;
        return new StringProcessor(
            content.substring(0, 1).toUpperCase() +
            content.substring(1).toLowerCase()
        );
    }

    public String getContent() {
        return content;
    }

    // Metodo fluent per concatenazione di operazioni
    public static StringProcessor of(String content) {
        return new StringProcessor(content);
    }
}

// Utilizzo fluent
String risultato = StringProcessor.of("  HELLO WORLD  ")
    .normalize()
    .removeSpaces()
    .capitalize()
    .getContent(); // "Helloworld"

Wrapper Classes

Tutte le wrapper classes (Integer, Double, Boolean, etc.) sono final per garantire l’integrità dei valori primitivi boxed e prevenire comportamenti inaspettati.

// Esempio di come potrebbe essere problematico se Integer non fosse final
// Se Integer non fosse final, qualcuno potrebbe fare:
/*
class MaliciousInteger extends Integer {
    public MaliciousInteger(int value) {
        super(value);
    }

    @Override
    public int intValue() {
        return super.intValue() + 1; // Modifica il valore!
    }
}
*/

// Invece, per estendere funzionalità numeriche, si usa composizione
public final class EnhancedInteger {
    private final Integer value;

    public EnhancedInteger(int value) {
        this.value = value;
    }

    public boolean isPrime() {
        if (value < 2) return false;
        if (value == 2) return true;
        if (value % 2 == 0) return false;

        for (int i = 3; i <= Math.sqrt(value); i += 2) {
            if (value % i == 0) return false;
        }
        return true;
    }

    public boolean isPerfect() {
        if (value <= 1) return false;

        int sum = 1;
        for (int i = 2; i <= Math.sqrt(value); i++) {
            if (value % i == 0) {
                sum += i;
                if (i != value / i) {
                    sum += value / i;
                }
            }
        }
        return sum == value;
    }

    public List<Integer> getFactors() {
        List<Integer> factors = new ArrayList<>();
        for (int i = 1; i <= Math.sqrt(value); i++) {
            if (value % i == 0) {
                factors.add(i);
                if (i != value / i) {
                    factors.add(value / i);
                }
            }
        }
        Collections.sort(factors);
        return factors;
    }

    public int getValue() {
        return value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

Design Patterns con Classi Final

Value Objects

Le classi final sono ideali per implementare value objects immutabili che rappresentano concetti del dominio.

public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = Objects.requireNonNull(amount);
        this.currency = Objects.requireNonNull(currency);

        if (amount.scale() > currency.getDefaultFractionDigits()) {
            throw new IllegalArgumentException(
                "Too many decimal places for currency " + currency.getCurrencyCode());
        }
    }

    public Money(double amount, Currency currency) {
        this(BigDecimal.valueOf(amount), currency);
    }

    public static Money euros(double amount) {
        return new Money(amount, Currency.getInstance("EUR"));
    }

    public static Money dollars(double amount) {
        return new Money(amount, Currency.getInstance("USD"));
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add money with different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot subtract money with different currencies");
        }
        return new Money(this.amount.subtract(other.amount), this.currency);
    }

    public Money multiply(BigDecimal factor) {
        return new Money(this.amount.multiply(factor), this.currency);
    }

    public Money multiply(double factor) {
        return multiply(BigDecimal.valueOf(factor));
    }

    public boolean isPositive() {
        return amount.compareTo(BigDecimal.ZERO) > 0;
    }

    public boolean isNegative() {
        return amount.compareTo(BigDecimal.ZERO) < 0;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public Currency getCurrency() {
        return currency;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Money money = (Money) obj;
        return Objects.equals(amount, money.amount) &&
               Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }

    @Override
    public String toString() {
        return String.format("%.2f %s", amount.doubleValue(), currency.getCurrencyCode());
    }
}

Configuration Classes

Le classi final sono eccellenti per rappresentare configurazioni immutabili.

public final class DatabaseConfiguration {
    private final String host;
    private final int port;
    private final String database;
    private final String username;
    private final char[] password; // Array per sicurezza
    private final int maxConnections;
    private final Duration timeout;
    private final boolean sslEnabled;

    private DatabaseConfiguration(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.database = builder.database;
        this.username = builder.username;
        this.password = builder.password.clone(); // Copia difensiva
        this.maxConnections = builder.maxConnections;
        this.timeout = builder.timeout;
        this.sslEnabled = builder.sslEnabled;
    }

    public static class Builder {
        private String host = "localhost";
        private int port = 5432;
        private String database;
        private String username;
        private char[] password;
        private int maxConnections = 10;
        private Duration timeout = Duration.ofSeconds(30);
        private boolean sslEnabled = true;

        public Builder host(String host) {
            this.host = Objects.requireNonNull(host);
            return this;
        }

        public Builder port(int port) {
            if (port <= 0 || port > 65535) {
                throw new IllegalArgumentException("Port must be between 1 and 65535");
            }
            this.port = port;
            return this;
        }

        public Builder database(String database) {
            this.database = Objects.requireNonNull(database);
            return this;
        }

        public Builder credentials(String username, char[] password) {
            this.username = Objects.requireNonNull(username);
            this.password = Objects.requireNonNull(password).clone();
            return this;
        }

        public Builder maxConnections(int maxConnections) {
            if (maxConnections <= 0) {
                throw new IllegalArgumentException("Max connections must be positive");
            }
            this.maxConnections = maxConnections;
            return this;
        }

        public Builder timeout(Duration timeout) {
            this.timeout = Objects.requireNonNull(timeout);
            return this;
        }

        public Builder sslEnabled(boolean sslEnabled) {
            this.sslEnabled = sslEnabled;
            return this;
        }

        public DatabaseConfiguration build() {
            if (database == null || username == null || password == null) {
                throw new IllegalStateException("Database, username and password are required");
            }
            return new DatabaseConfiguration(this);
        }
    }

    // Getter methods
    public String getHost() { return host; }
    public int getPort() { return port; }
    public String getDatabase() { return database; }
    public String getUsername() { return username; }
    public char[] getPassword() { return password.clone(); } // Copia difensiva
    public int getMaxConnections() { return maxConnections; }
    public Duration getTimeout() { return timeout; }
    public boolean isSslEnabled() { return sslEnabled; }

    public String getConnectionString() {
        return String.format("jdbc:postgresql://%s:%d/%s?ssl=%s",
                            host, port, database, sslEnabled);
    }

    // Cleanup method per sicurezza
    public void clearPassword() {
        Arrays.fill(password, '\0');
    }

    @Override
    public String toString() {
        return String.format("DatabaseConfiguration{host='%s', port=%d, database='%s', username='%s', ssl=%s}",
                            host, port, database, username, sslEnabled);
    }
}

Ottimizzazioni delle Performance

Le classi final permettono al compilatore e alla JVM di applicare ottimizzazioni specifiche che non sarebbero possibili con classi estensibili.

Inlining dei Metodi

public final class MathUtils {
    // Metodi che possono essere facilmente inlined
    public static final double PI = 3.14159265359;

    public final double square(double x) {
        return x * x; // Facilmente inlineable
    }

    public final double circleArea(double radius) {
        return PI * square(radius); // Entrambe le chiamate possono essere inlined
    }

    public final boolean isEven(int number) {
        return (number & 1) == 0; // Ottimizzazione bitwise
    }

    public final int fastModulo(int value, int powerOfTwo) {
        // Ottimizzazione per modulo con potenze di 2
        return value & (powerOfTwo - 1);
    }
}

// Benchmark per dimostrare le ottimizzazioni
public final class PerformanceDemo {
    private static final MathUtils mathUtils = new MathUtils();

    public static void benchmarkCalculations() {
        long start = System.nanoTime();

        double result = 0;
        for (int i = 0; i < 1_000_000; i++) {
            result += mathUtils.circleArea(i); // Chiamate che possono essere inlined
        }

        long end = System.nanoTime();
        System.out.println("Risultato: " + result);
        System.out.println("Tempo: " + (end - start) / 1_000_000 + " ms");
    }
}

Ottimizzazioni della JVM

public final class OptimizedContainer<T> {
    private final T[] elements;
    private final int size;

    @SuppressWarnings("unchecked")
    public OptimizedContainer(T[] elements) {
        this.elements = (T[]) new Object[elements.length];
        System.arraycopy(elements, 0, this.elements, 0, elements.length);
        this.size = elements.length;
    }

    // Metodo final che può essere ottimizzato aggressivamente
    public final T get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        return elements[index]; // Accesso diretto ottimizzabile
    }

    public final int size() {
        return size; // Semplice accesso a campo final
    }

    public final boolean isEmpty() {
        return size == 0; // Ottimizzabile a livello di istruzione
    }

    // Iterazione ottimizzata
    public final void forEach(Consumer<T> action) {
        Objects.requireNonNull(action);
        for (int i = 0; i < size; i++) {
            action.accept(elements[i]); // Loop ottimizzabile
        }
    }
}

Sicurezza e Controllo dell’Accesso

Le classi final giocano un ruolo importante nella sicurezza dell’applicazione prevenendo modifiche non autorizzate attraverso l’ereditarietà.

public final class SecureToken {
    private final String value;
    private final Instant expiration;
    private final Set<String> permissions;

    public SecureToken(String value, Instant expiration, Set<String> permissions) {
        this.value = Objects.requireNonNull(value);
        this.expiration = Objects.requireNonNull(expiration);
        this.permissions = Set.copyOf(permissions); // Copia immutabile

        if (expiration.isBefore(Instant.now())) {
            throw new IllegalArgumentException("Token cannot be created with past expiration");
        }
    }

    public boolean isValid() {
        return Instant.now().isBefore(expiration);
    }

    public boolean hasPermission(String permission) {
        return isValid() && permissions.contains(permission);
    }

    public String getValue() {
        if (!isValid()) {
            throw new SecurityException("Token has expired");
        }
        return value;
    }

    public Set<String> getPermissions() {
        if (!isValid()) {
            throw new SecurityException("Token has expired");
        }
        return permissions; // Già immutabile
    }

    public Duration getTimeRemaining() {
        return Duration.between(Instant.now(), expiration);
    }

    // Factory method per creare token con scadenza relativa
    public static SecureToken createWithDuration(String value, Duration validity, Set<String> permissions) {
        return new SecureToken(value, Instant.now().plus(validity), permissions);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        SecureToken that = (SecureToken) obj;
        return Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return String.format("SecureToken{valid=%s, permissions=%d, expires=%s}",
                           isValid(), permissions.size(), expiration);
    }
}

Alternative alla Final Class

Quando una classe final potrebbe essere troppo restrittiva, esistono alternative che mantengono controllo pur permettendo estensibilità limitata.

Composition over Inheritance

// Invece di rendere final, usa composition
public class FlexibleStringProcessor {
    private final StringProcessor processor;

    public FlexibleStringProcessor(String content) {
        this.processor = new StringProcessor(content);
    }

    // Delega le operazioni base
    public FlexibleStringProcessor normalize() {
        return new FlexibleStringProcessor(processor.normalize().getContent());
    }

    public FlexibleStringProcessor removeSpaces() {
        return new FlexibleStringProcessor(processor.removeSpaces().getContent());
    }

    // Aggiunge nuove funzionalità
    public FlexibleStringProcessor reverseWords() {
        String[] words = processor.getContent().split("\\s+");
        Collections.reverse(Arrays.asList(words));
        return new FlexibleStringProcessor(String.join(" ", words));
    }

    public FlexibleStringProcessor encrypt(String key) {
        // Implementazione semplificata di encryption
        StringBuilder encrypted = new StringBuilder();
        for (int i = 0; i < processor.getContent().length(); i++) {
            char c = processor.getContent().charAt(i);
            char keyChar = key.charAt(i % key.length());
            encrypted.append((char) (c ^ keyChar));
        }
        return new FlexibleStringProcessor(encrypted.toString());
    }

    public String getContent() {
        return processor.getContent();
    }
}

Protected Constructor Pattern

public class ControlledExtension {
    private final String data;

    // Constructor protetto permette estensione controllata
    protected ControlledExtension(String data) {
        this.data = Objects.requireNonNull(data);
    }

    // Factory method pubblico per controllo della creazione
    public static ControlledExtension create(String data) {
        validateData(data);
        return new ControlledExtension(data);
    }

    private static void validateData(String data) {
        if (data == null || data.trim().isEmpty()) {
            throw new IllegalArgumentException("Data cannot be null or empty");
        }
    }

    public String getData() {
        return data;
    }

    // Metodi che possono essere override con controllo
    protected void onDataAccess() {
        // Hook per sottoclassi
    }

    public final String getProcessedData() {
        onDataAccess();
        return processData();
    }

    protected String processData() {
        return data.toUpperCase(); // Comportamento di default
    }
}

// Estensione controllata
class ExtendedProcessor extends ControlledExtension {
    protected ExtendedProcessor(String data) {
        super(data);
    }

    public static ExtendedProcessor createExtended(String data) {
        validateExtendedData(data);
        return new ExtendedProcessor(data);
    }

    private static void validateExtendedData(String data) {
        if (data != null && data.length() > 1000) {
            throw new IllegalArgumentException("Extended data cannot exceed 1000 characters");
        }
    }

    @Override
    protected String processData() {
        return getData().toLowerCase().replace(" ", "_");
    }

    @Override
    protected void onDataAccess() {
        System.out.println("Accessing extended data: " + getData().substring(0, Math.min(10, getData().length())));
    }
}

Best Practices

Usa Final per Value Objects: Rendi sempre final le classi che rappresentano valori immutabili come coordinate, money, date ranges, etc.

Considera Final per Utility Classes: Classi che contengono solo metodi statici dovrebbero spesso essere final e avere un costruttore privato.

Documenta il Rationale: Quando rendi una classe final, documenta chiaramente perché hai preso questa decisione di design.

Combina con Immutabilità: Le classi final sono spesso anche immutabili; assicurati che tutti i campi siano final e non ci siano setter.

Usa Composition per Estensibilità: Se hai bisogno di estendere funzionalità di una classe final, usa composition invece di ereditarietà.

Considera le Performance: Sfrutta le ottimizzazioni possibili con classi final, specialmente in codice critico per le performance.

Factory Methods: Usa factory methods invece di costruttori pubblici per controllare la creazione di istanze.

Conclusione

Le classi final sono uno strumento potente per creare design software robusti e sicuri. Prevenendo l’ereditarietà, garantiscono l’integrità del comportamento della classe e permettono ottimizzazioni significative delle performance.

La decisione di rendere final una classe dovrebbe essere basata su considerazioni di design chiare: necessità di immutabilità, requisiti di sicurezza, ottimizzazioni delle performance, o semplicemente il fatto che la classe rappresenta un concetto completo che non dovrebbe essere esteso.

Quando utilizzate appropriatamente, le classi final contribuiscono a creare codice più prevedibile, sicuro e performante, elementi fondamentali per applicazioni enterprise robuste.