Conoscere e gestire le peculiarità di Java.
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.
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.
Nella maggior parte dei casi, chiamare un metodo di una classe prevede anche il passaggio di un certo numero di parametri.
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
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...
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.
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...
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.
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.
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.
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.
Loading diagram...
src
├── Main.java
└── entity
├── Character.java
├── Damageable.java
├── Enemy.java
├── Entity.java
├── Ghost.java
├── Hero.java
├── Interactable.java
├── LightSwitch.java
└── Orc.java
javac Main.java
java Main
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 entrypointIl 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.
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.
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
.
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.
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.
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.
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 (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.
I generics sono un meccanismo che permette di creare classi e metodi parametrici, in grado di operare su qualsiasi tipo non primitivo.
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.
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.
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
.
I record sono un tipo particolare di classe che permette di creare oggetti immutabili con pochissimo boilerplate.
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 + '\'' +
']';
}
}
Il costrutto switch
è un'alternativa all'if-else
per la gestione di casi multipli.
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
.
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
.
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
.