Cos’è un secondary constructor?
In Kotlin, la costruzione degli oggetti avviene principalmente usando il primary constructor (cioè il costruttore principale). Tuttavia, esistono situazioni in cui hai bisogno di costruire oggetti in modi alternativi o con parametri differenti: qui entra in gioco il secondary constructor.
Un secondary constructor serve per fornire più modi di istanziare una classe, offrendo flessibilità nella creazione degli oggetti.
- Fornire modi alternativi per creare un’istanza della classe
- Supportare più forme di inizializzazione
- Interoperabilità con codice Java (in alcuni casi)
Regole Sintattiche
- Si dichiara usando la parola chiave
constructor
- Deve chiamare direttamente o indirettamente il costruttore primario (se esiste)
- Può avere parametri diversi dal costruttore primario
- Può avere un corpo per eseguire logica aggiuntiva
- Nel secondo costruttore non è possibile anteporre Val o Var avanti al nome delle variabili
- Nel secondo costruttore non puoi scrivere codice per una varibiale che dichiari devi aggirare il tutto con una variabile dichiarata nullable nel primo e utilizzata come se fosse un parametro
class NomeClasse(parametriPrimari) {
constructor(parametriSecondari) : this(argomentiPerPrimario) {
// corpo del costruttore secondario
}
}
Classe senza costruttore primario ma con costruttore secondario
class NomeClasse {
constructor(param1: Tipo1, param2: Tipo2) {
// corpo del costruttore
}
}
Se la classe ha anche un primary constructor, il secondary constructor deve delegare a esso usando this(…).
class Persona(val nome: String) {
var eta: Int = 0
constructor(nome: String, eta: Int) : this(nome) {
this.eta = eta
}
}
Esempi Pratici
class Utente(val nome: String, val email: String) {
var dataDiNascita: String? = null
// Secondary constructor
constructor(nome: String, email: String, dataDiNascita_Param: String) : this(nome, email) {
dataDiNascita = dataDiNascita_Param
// è equivalente a
// this.dataDiNascita = dataDiNascita
}
fun stampaInformazioni() {
println("Nome: $nome")
println("Email: $email")
println("Data di nascita: ${dataDiNascita ?: "Non fornita"}")
// con tanto di esempio di operatore Elvis
}
}
fun main() {
val utente1 = Utente("Marco", "marco@email.com")
val utente2 = Utente("Anna", "anna@email.com", "1990-05-12")
// Che stampa se su esegue:
/* Nome: Marco
Email: marco@email.com
Data di nascita: Non fornita
---
Nome: Anna
Email: anna@email.com
Data di nascita: 1990-05-12 */
Esempio: Classe Studente
con più secondi costruttori e init
class Studente(val nome: String, val cognome: String) {
var eta: Int = -1
var scuola: String = "Non specificata"
// Blocco di inizializzazione
init {
println("Studente creato: $nome $cognome")
}
// Secondary constructor con nome, cognome, eta
constructor(nome: String, cognome: String, eta: Int) : this(nome, cognome) {
this.eta = eta
}
// Secondary constructor con nome, cognome, eta, scuola
constructor(nome: String, cognome: String, eta: Int, scuola: String) : this(nome, cognome, eta) {
this.scuola = scuola
}
fun stampaInfo() {
println("Nome: $nome $cognome")
println("Età: ${if (eta >= 0) eta else "Non fornita"}")
println("Scuola: $scuola")
}
}
fun main() {
val s1 = Studente("Luca", "Verdi")
val s2 = Studente("Anna", "Rossi", 16)
val s3 = Studente("Giulia", "Bianchi", 18, "Liceo Scientifico")
println("---")
s1.stampaInfo()
println("---")
s2.stampaInfo()
println("---")
s3.stampaInfo()
}
// Fornisce:
Studente creato: Luca Verdi
Studente creato: Anna Rossi
Studente creato: Giulia Bianchi
---
Nome: Luca Verdi
Età: Non fornita
Scuola: Non specificata
---
Nome: Anna Rossi
Età: 16
Scuola: Non specificata
---
Nome: Giulia Bianchi
Età: 18
Scuola: Liceo Scientifico
In questo caso, scuola non è stato dichiarato nullable perchè assume il valore “non specificata”, ma puoi essere passato come parametro nel terzo costruttore e in quel caso stampa il suo valore
Altri esempi
Nel seguente esempio il secondo costruttore ha lo scopo di utilizzare una variabile in meno e non di aggiungerne una
class Persona(val nome: String, val eta: Int) {
// Costruttore secondario che accetta solo il nome
constructor(nome: String) : this(nome, 0) {
println("Costruttore secondario chiamato con nome: $nome, $eta")
}
}
// Utilizzo
fun main() {
val persona1 = Persona("Mario", 30) // usa costruttore primario
val persona2 = Persona("Luigi") // usa costruttore secondario (eta=0)
}
// restituisce:
Costruttore secondario chiamato con nome: Luigi, 0
Esempio 3: Logica aggiuntiva nel costruttore secondario
class Rettangolo(val larghezza: Int, val altezza: Int) {
// Costruttore per creare un quadrato
constructor(lato: Int) : this(lato, lato)
// Costruttore senza parametri (valori di default)
constructor() : this(1, 1)
}
// Utilizzo
fun main() {
val ret1 = Rettangolo(10, 20) // rettangolo normale
val quad = Rettangolo(15) // quadrato (15x15)
val def = Rettangolo() // 1x1
}
Esempio 3: Logica aggiuntiva nel costruttore secondario
class Utente(val username: String, val password: String) {
init {
require(password.length >= 8) {
"La password deve contenere almeno 8 caratteri (ricevuti ${password.length})" // questo non stampa a video ma è un break point che produce questo messaggio
}
require(password.any { it.isDigit() }) {
"La password deve contenere almeno un numero"
// anche questo produce un break point del tipo:
// Exception in thread "main" java.lang.IllegalArgumentException: La password deve contenere almeno un numero
}
println("Username: $username, Password: $password")
}
// Costruttore secondario che genera una password casuale
constructor(username: String) : this(username, generateRandomPassword()) {
println("Password generata per $username")
}
companion object {
private fun generateRandomPassword(): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return (1..10).map { chars.random() }.joinToString("")
}
}
}
// Utilizzo
fun main() {
val utente1 = Utente("admin", "passwordSegreta") // costruttore primario
val utente2 = Utente("guest") // costruttore secondario con password generata
}
// produce a video:
Username: admin, Password: passwordSegreta1
Username: guest, Password: tSfpP6eMjM
Password generata per guest
Spiegazione della parte del companion object:
Spiegazione riga per riga:companion object
: In Kotlin, un companion object
è un costrutto speciale che ti permette di definire membri statici (come metodi e proprietà statiche) all’interno di una classe, senza dover creare un’istanza di quella classe per accedervi. È simile al concetto di static
in Java o C#. Tutte le funzioni o le proprietà definite all’interno di un companion object
appartengono alla classe stessa, non a una sua istanza specifica.private fun generateRandomPassword(): String {
private
: Questo è un modificatore di visibilità. Indica che la funzione generateRandomPassword
può essere chiamata o acceduta solamente dall’interno della classe in cui è definita (o dal suo companion object
). Non può essere chiamata da codice esterno a quella classe.generateRandomPassword
: Questo è il nome della funzione. È un nome descrittivo che indica chiaramente lo scopo della funzione: generare una password casuale.val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
val
: Questa parola chiave è usata per dichiarare una variabile immutabile (read-only) in Kotlin. Significa che una volta che chars
viene inizializzata, il suo valore non può essere cambiato."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
: Questa è una stringa di testo. Contiene tutti i caratteri che possono essere utilizzati per comporre la password casuale: lettere maiuscole, lettere minuscole e cifre numeriche. Questa stringa funge da “pool” di caratteri da cui pescare.return (1..10).map { chars.random() }.joinToString("")
return
: Parola chiave che indica che il valore risultante di questa espressione verrà restituito dalla funzione generateRandomPassword
.(1..10)
: Questa è una “range expression” (intervallo) in Kotlin. Crea un intervallo di numeri interi da 1 a 10 (inclusi). Concettualmente, questo rappresenta che vogliamo generare una sequenza di 10 caratteri..map { chars.random() }
:.map { ... }
: È una funzione di ordine superiore (Higher-Order Function) che si applica a una collezione (in questo caso, l’intervallo da 1 a 10). Per ogni elemento nell’intervallo, esegue l’espressione all’interno delle parentesi graffe {}
e raccoglie i risultati in una nuova lista.chars.random()
: Questa è un’estensione di funzione sulle stringhe in Kotlin. Seleziona un carattere casuale dalla stringa chars
. Quindi, per ogni numero da 1 a 10, questa espressione seleziona un carattere casuale dalla stringa chars
. Il risultato di .map
sarà una lista di 10 caratteri casuali..joinToString(...)
: Questa è un’altra funzione di estensione, applicata alla lista di caratteri generata da map
. Concatena tutti gli elementi di una collezione (in questo caso, i 10 caratteri casuali) in una singola stringa.
Quando usare i costruttori secondari
- Valori di default complessi: Quando i valori di default richiedono calcoli o logica
- Conversioni di tipo: Quando vuoi accettare parametri di tipo diverso
- Interoperabilità con Java: In casi particolari con librerie Java
- Legacy code: Quando lavori con codice esistente che usa questo pattern
Alternative ai Costruttori Secondari
In Kotlin, spesso è preferibile usare:
- Parametri di default nel costruttore primario
- Funzioni factory nella companion object