Conoscere e gestire le peculiarità di Java.
Obiettivi
- Casting
- Valore vs Riferimento
- Confronto fra oggetti
- Progetti con più file
- Polimorfismo
- Eccezioni
- Generics
- Record
Casting
public static void main(String[] args) {
int x = 10;
double y = 5.5;
x = (int) y;
}
Java è un linguaggio staticamente tipizzato.
Generalmente non è permesso convertire automaticamente un oggetto di tipo in un altro.
Il casting deve essere esplicito, ed è responsabilità del programmatore.
Casting fra oggetti
class Shape{}
class Circle extends Shape{}
public static void main(String[] args) {
Circle c = new Circle();
// Il cerchio è una sottclasse di Shape
Shape s = c;
// Controllo sul tipo di s
if (s instanceof Circle){
Circle c2 = (Circle) s;
}
}
Prima di fare un casting, è buona norma assicurarsi che l'oggetto sia del tipo atteso.
Il casting implicito è consentito da una classe derivata alla superclasse, mentre viceversa l'operazione deve essere forzata esplicitamente.
Passaggio di parametri
Nella maggior parte dei casi, chiamare un metodo di una classe prevede anche il passaggio di un certo numero di parametri.
Passaggio per valore
Passare un parametro per valore crea una nuova variabile che ha lo stesso valore dell'originale.
Le due variabili sono indipendenti.
Sono passati per valore tutti i tipi primitivi: boolean, byte, char, short, int, long, float, double
Esempio: passaggio per valore
public static int sum(int a, int b){
a = a + b;
return a;
}
public static void main(String args[]){
int x = 1, y = 5;
sum(x, y);
}
Loading diagram...
Loading diagram...
Loading diagram...
Passaggio per riferimento
Passare un parametro pre riferimento crea una nuova variabile che punta allo stesso oggetto di quella originale.
Entrambe le variabili permettono di agire sullo stesso oggetto.
Sono passati per riferimento tutti gli oggetti.
Esempio: passaggio per riferimento
public static int appendIntAndGetSize(List<String> list, int value) {
list.add(String.valueOf(value));
return list.size();
}
public static void main(String[] args) {
int val = 12;
List<String> arrayList = new ArrayList<>();
arrayList.add("10");
appendIntAndGetSize(arrayList, val);
}
Loading diagram...
Loading diagram...
Loading diagram...
Confronto fra oggetti
public class Circle{
public int radius;
public Circle(int radius){
this.radius = radius;
}
}
In Java, tutto ciò che non è un tipo primitivo è gestito tramite puntatori.
Fare un confronto fra due oggetti con obj1 == obj2
significa confrontare i puntatori.
Metodo equals()
class Circle{
// ...
@Override
public boolean equals(Object obj){
if (obj == null) return false;
if (obj == this) return true;
if (!(obj instanceof Circle)) return false;
Circle other = (Circle) obj;
return this.radius == other.radius;
}
}
Per fare un confronto logico fra due oggetti, è necessario sovrascrivere il metodo equals()
.
La logica può essere complessa a piacimento, ma generalmente si controllano i tipi dell'oggetto e i valori dei campi della classe.
Lavorare con più classi
Le classi raggruppano dati e metodi in un unico blocco logico.
Un progetto complesso conterrà un numero elevato di classi.
I design pattern sono una serie di regole che permettono di organizzare le classi in modo da avere un codice più pulito e mantenibile.
Suddividere le classi in package
La convenzione prevede che ogni file contenga una classe con lo stesso nome (ad eccezioni delle classi interne ad altre).
Le classi possono essere raggruppate in package.
Un package è una cartella che contiene una serie di classi che rappresentano un aspetto del progetto.
Esempio: suddividere le classi in package
Loading diagram...
src
├── Main.java
└── entity
├── Character.java
├── Damageable.java
├── Enemy.java
├── Entity.java
├── Ghost.java
├── Hero.java
├── Interactable.java
├── LightSwitch.java
└── Orc.java
Compilazione ed esecuzione
javac Main.java
java Main
Creare un archivio JAR
Un archivio JAR è un file che contiene una serie di classi e risorse.
Nel caso di progetti composti da più file, può essere utile creare un archivio JAR.
javac Main.java
jar cvfe Main.jar Main Main.class entity/*.class
c
: crea un nuovo archiviov
: sii verboso nell'outputf
: specifica il nome del file JAR prodottoe
: specifica la classe entrypoint
Polimorfismo
Il polimorfismo è un concetto fondamentale della programmazione orientata agli oggetti.
Un oggetto può essere visto in maniera diversa, più o meno generica, a seconda delle esigenze.
Ciò che permette di raggiungere questo risultato sono le meccaniche di ereditarietà e implementazione.
Utilizzare il polimorfismo: raggruppare
List<Entity> entities = new ArrayList<>();
entities.add(new LightSwitch());
entities.add(new Orc());
entities.add(new Ghost());
Utilizzare la classe base permette di raggruppare gli oggetti in un'unica struttura.
Questo perché, sebbene i tipi siano diversi, tutti derivano dalla stessa classe base.
Utilizzare il polimorfismo: differenziare
for (Entity entity : entities) {
if (entity instanceof Interactable) {
Interactable interactable = (Interactable) entity;
interactable.interact();
}
if (entity instanceof Damageable) {
hero.attack((Damageable) entity);
}
if (entity instanceof Enemy enemy) {
if (enemy.isAlive())
enemy.attack(hero);
}
}
È comunque possibile differenziare gli oggetti in base alle loro caratteristiche specifiche.
Per farlo, è necessario effettuare un cast esplicito, assicurandosi che l'oggetto sia effettivamente di quel tipo con un'istruzione instanceof
.
Eccezioni
Le eccezioni sono un meccanismo che permette di gestire gli errori in modo controllato.
In Java, le eccezioni sono oggetti che vengono lanciati quando si verifica un errore, interrompendo il flusso normale dell'esecuzione.
Gestire le eccezioni
try {
// ...
} catch (IOException | SQLException ex) {
// ...
} finally {
// ...
}
Le eccezioni possono essere gestite con un blocco try-catch
.
Il blocco try
contiene il codice che può generare un'eccezione.
Il blocco catch
contiene il codice che viene eseguito solo se viene generata un'eccezione del tipo specificato.
Il blocco finally
contiene il codice che viene eseguito in ogni caso.
Lanciare eccezioni
throw new Exception("Something went wrong");
È possibile lanciare eccezioni con l'istruzione throw
accompagnato da un oggetto che estende la classe Exception
.
Se si vuole creare una nuova eccezione per un caso d'uso personalizzato, è possibile estendere la classe Exception
o una delle sue sottoclassi.
Delegare la gestione delle eccezioni
public void interact() throws Exception {
throw new Exception("Something went wrong");
}
Normalmente, quando si maneggia del codice che può generare un'eccezione, è necessario gestirla all'interno del blocco try-catch
.
Se invece si vuole delegare la gestione dell'eccezione, è possibile aggiungere la clausola throws
al metodo, indicando il tipo di eccezione che può essere generata.
Try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine()
// ...
} catch (Exception e) {
// ...
}
La struttura try-with-resources
permette di gestire le risorse che vengono aperte all'interno di un blocco try
.
Le risorse vengono automaticamente chiusa alla fine del blocco try
, anche in caso di eccezioni.
Eccezioni: best practice
- Be specific: catturare eccezioni che sappiamo come gestire
- Fail-fast: notificare l'errore il prima possibile
- Catch-late: rimandare la gestione dell'errore al livello superiore
- Logging: registrare gli errori in un file di log
- Custom exceptions: creare eccezioni personalizzate per i casi d'uso specifici
- Use judiciously: evitare di usare eccezioni per il controllo del flusso
Generics
I generics sono un meccanismo che permette di creare classi e metodi parametrici, in grado di operare su qualsiasi tipo non primitivo.
Creare una classe generica
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
All'interno della definizione della classe, è possibile specificare uno o più tipi generici fra le parentesi angolate.
Utilizzare una classe generica
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<>("Hello", 42);
System.out.println(pair.getKey());
System.out.println(pair.getValue());
}
Nel momento in cui si crea un oggetto di una classe generica, è necessario specificare i tipi con cui si vuole utilizzare la classe.
Solo i tipi non primitivi possono essere usati come tipi generici.
Limitare i tipi generici
public class Pair<K extends Comparable<K>, V> {
// ...
}
public class Box<K super Serializable & Iterable> {
// ...
}
È possibile specificare un limite superiore o inferiore per i tipi generici in termini di ereditarietà.
Nel caso di Pair, il tipo K
deve implementare l'interfaccia Comparable
.
Record
I record sono un tipo particolare di classe che permette di creare oggetti immutabili con pochissimo boilerplate.
Creare un record
public record Item(int price, String name) {}
Questa sintassi equivale a:
public class Item {
private int price;
private String name;
public Item(int price, String name) {
this.price = price;
this.name = name;
}
public int getPrice() {
return price;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Item item = (Item) o;
return price == item.price && Objects.equals(name, item.name);
}
@Override
public int hashCode() {
return Objects.hash(price, name);
}
@Override
public String toString() {
return "Item[" +
"price=" + price +
", name='" + name + '\'' +
']';
}
}
Switch
Il costrutto switch
è un'alternativa all'if-else
per la gestione di casi multipli.
Sintassi classica
switch (value) {
case 1:
// ...
break;
case 2:
// ...
break;
default:
// ...
}
Il costrutto switch
prende in input un'espressione e confronta il suo valore con i valori specificati nei case
.
Nel caso in cui il valore dell'espressione corrisponda a uno dei valori specificati, viene eseguito il codice contenuto nel case
, proseguendo nelle sezioni sottostanti fino a che non si incontra un break
.
Sintassi compatta
switch (value) {
case 1, 2, 3 -> System.out.println("Small");
case 4, 5, 6 -> System.out.println("Medium");
case 7, 8, 9 -> System.out.println("Large");
default -> System.out.println("Unknown");
}
Nelle nuove versioni di Java è possibile utilizzare la sintassi più compatta, che evita la necessità di utilizzare il break
.
Sintassi con yield
int result = switch (value) {
case 1, 2, 3 -> 1;
case 4, 5, 6 -> 2;
case 7, 8, 9 -> 3;
default -> {
System.out.println("Unexpected value");
yield 0;
};
};
Lo switch può anche restituisce il valore dell'espressione contenuta nel case
corrispondente, permettendo ad esempio di memorizzare il risultato in una variabile.
Se si ha la necessità di eseguire più istruzioni all'interno di un case
, è possibile utilizzare un blocco di codice e restituire il valore finale con la keyword yield
.
Challenge
- Creare una coda utilizzando i generics
- Utilizzare la classe ArrayList per implementare lo stack o la coda e che utilizzi le eccezioni per gestire i casi limite
- Creare una coda di priorità generica che si specializza in max-heap o min-heap in base alla sottoclasse
- Creare un wrapper per la lettura e scrittura di file
- Creare una struttura dati simile a Pair che permetta di memorizzare più di due elementi
- Creare un simulatore di gioco di ruolo molto semplice da riga di comando