JavaRush /Java Blog /Random-IT /Pausa caffè #155. Le 10 principali funzioni di Java

Pausa caffè #155. Le 10 principali funzioni di Java

Pubblicato nel gruppo Random-IT

Le 10 principali funzioni di Java

Fonte: DZone Questo articolo elenca dieci funzionalità di programmazione Java che vengono spesso utilizzate dagli sviluppatori nel loro lavoro quotidiano. Pausa caffè #155.  Le 10 principali funzioni di Java - 1

1. Metodo della fabbrica di raccolta

Le raccolte sono una delle funzionalità più comunemente utilizzate nella programmazione. Sono usati come contenitori in cui conserviamo oggetti e li trasmettiamo. Le raccolte vengono utilizzate anche per ordinare, cercare e ripetere oggetti, rendendo la vita di un programmatore molto più semplice. Hanno diverse interfacce di base come List, Set, Map, oltre a diverse implementazioni. Il modo tradizionale di creare raccolte e mappe può sembrare prolisso a molti sviluppatori. Ecco perché Java 9 ha introdotto diversi metodi factory concisi. Elenco :
List countries = List.of("Bangladesh", "Canada", "United States", "Tuvalu");
Impostato :
Set countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Carta geografica :
Map countriesByPopulation = Map.of("Bangladesh", 164_689_383,
                                                            "Canada", 37_742_154,
                                                            "United States", 331_002_651,
                                                            "Tuvalu", 11_792);
Il metodo factory è molto utile quando vogliamo creare contenitori immutabili. Ma se intendi creare raccolte modificabili, ti consigliamo di utilizzare l'approccio tradizionale.

2. Inferenza di tipo locale

Java 10 ha aggiunto l'inferenza del tipo per le variabili locali. Prima di ciò, gli sviluppatori dovevano specificare i tipi due volte durante la dichiarazione e l'inizializzazione di un oggetto. Era molto faticoso. Guarda il seguente esempio:
Map> properties = new HashMap<>();
Il tipo di informazioni su entrambi i lati è indicato qui. Se lo definiamo in un unico posto, il lettore di codice e il compilatore Java capiranno facilmente che deve essere di tipo Map. L'inferenza del tipo locale fa proprio questo. Ecco un esempio:
var properties = new HashMap>();
Ora tutto viene scritto una sola volta e il codice non sembra molto peggiore. E quando chiamiamo un metodo e memorizziamo il risultato in una variabile, il codice diventa ancora più breve. Esempio:
var properties = getProperties();
E inoltre:
var countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Sebbene l'inferenza del tipo locale sembri una funzionalità conveniente, alcune persone la criticano. Alcuni sviluppatori sostengono che ciò riduce la leggibilità. E questo è più importante della brevità.

3. Espressioni di cambio avanzate

La tradizionale istruzione switch è presente in Java fin dall'inizio e all'epoca ricordava C e C++. Questo andava bene, ma con l'evoluzione del linguaggio, questo operatore non ci ha offerto alcun miglioramento fino a Java 14. Naturalmente presentava alcuni svantaggi. Il più noto è stato il fall -through: per risolvere questo problema, gli sviluppatori hanno utilizzato istruzioni break, che sono in gran parte codice standard. Tuttavia, Java 14 ha introdotto una versione migliorata dell'istruzione switch con un elenco di funzioni molto più ampio. Ora non abbiamo più bisogno di aggiungere istruzioni break e questo risolve il problema del fallimento. Inoltre, un'istruzione switch può restituire un valore, il che significa che possiamo usarlo come espressione e assegnarlo a una variabile.
int day = 5;
String result = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unexpected value: " + day;
};

4. Registrazioni

Sebbene Records sia una funzionalità relativamente nuova introdotta in Java 16, molti sviluppatori la trovano molto utile, principalmente per la creazione di oggetti immutabili. Spesso abbiamo bisogno di oggetti dati nel nostro programma per archiviare o passare valori da un metodo all'altro. Ad esempio, una classe per il trasferimento delle coordinate x, yez, che scriveremo come segue:
package ca.bazlur.playground;

import java.util.Objects;

public final class Point {
    private final int x;
    private final int y;
    private final int z;

    public Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    public int z() {
        return z;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (Point) obj;
        return this.x == that.x &&
                this.y == that.y &&
                this.z == that.z;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y, z);
    }

    @Override
    public String toString() {
        return "Point[" +
                "x=" + x + ", " +
                "y=" + y + ", " +
                "z=" + z + ']';
    }

}
La lezione sembra troppo prolissa. Con l'aiuto delle voci, tutto questo codice può essere sostituito con una versione più concisa:
package ca.bazlur.playground;

public record Point(int x, int y, int z) {
}

5.Facoltativo

Un metodo è un contratto in cui definiamo le condizioni. Specifichiamo i parametri con il loro tipo, nonché il tipo restituito. Ci aspettiamo quindi che quando viene chiamato il metodo, si comporti secondo il contratto. Tuttavia, spesso ci ritroviamo con un metodo null invece di un valore del tipo specificato. Questo è un errore. Per risolvere questo problema, l'iniziatore in genere verifica il valore con una condizione if, indipendentemente dal fatto che il valore sia null o meno. Esempio:
public class Playground {

    public static void main(String[] args) {
        String name = findName();
        if (name != null) {
            System.out.println("Length of the name : " + name.length());
        }
    }

    public static String findName() {
        return null;
    }
}
Guarda il codice sopra. Il metodo findName dovrebbe restituire un String , ma restituisce null. L'iniziatore deve ora verificare prima la presenza di valori null per gestire il problema. Se l'iniziatore si dimentica di farlo, finiremo per ottenere una NullPointerException . D'altra parte, se la firma del metodo indicasse la possibilità di mancato ritorno, allora questo risolverebbe tutta la confusione. Ed è qui che l'Optional ci può aiutare .
import java.util.Optional;

public class Playground {

    public static void main(String[] args) {
        Optional optionalName = findName();
        optionalName.ifPresent(name -> {
            System.out.println("Length of the name : " + name.length());
        });
    }

    public static Optional findName() {
        return Optional.empty();
    }
}
Qui abbiamo riscritto il metodo findName con un'opzione opzionale per non restituire alcun valore. Ciò avvisa i programmatori in anticipo e risolve il problema.

6. API Data/Ora Java

Ogni sviluppatore è confuso in un modo o nell'altro con il calcolo della data e dell'ora. Questa non è un'esagerazione. Ciò era dovuto principalmente alla mancanza di una buona API Java per lavorare con date e orari. Ora questo problema non è più rilevante, perché Java 8 ha introdotto un eccellente set di API nel pacchetto java.time, che risolve tutti i problemi relativi a data e ora. Il pacchetto java.time ha molte interfacce e classi che eliminano la maggior parte dei problemi, inclusi i fusi orari. Le classi più comunemente utilizzate in questo pacchetto sono:
  • LocalDate
  • Ora locale
  • LocalDateTime
  • Durata
  • Periodo
  • ZonedDateTime
Un esempio di utilizzo delle classi dal pacchetto java.time:
import java.time.LocalDate;
import java.time.Month;

public class Playground3 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2022, Month.APRIL, 4);
        System.out.println("year = " + date.getYear());
        System.out.println("month = " + date.getMonth());
        System.out.println("DayOfMonth = " + date.getDayOfMonth());
        System.out.println("DayOfWeek = " + date.getDayOfWeek());
        System.out.println("isLeapYear = " + date.isLeapYear());
    }
}
Un esempio di utilizzo della classe LocalTime per calcolare l'ora:
LocalTime time = LocalTime.of(20, 30);
int hour = time.getHour();
int minute = time.getMinute();
time = time.withSecond(6);
time = time.plusMinutes(3);
Aggiunta di un fuso orario:
ZoneId zone = ZoneId.of("Canada/Eastern");
LocalDate localDate = LocalDate.of(2022, Month.APRIL, 4);
ZonedDateTime zonedDateTime = date.atStartOfDay(zone);

7.NullPointerException

Ogni sviluppatore odia NullPointerException. Può essere particolarmente difficile quando StackTrace non fornisce informazioni utili su quale sia esattamente il problema. Per dimostrarlo, diamo un'occhiata al codice di esempio:
package com.bazlur;

public class Main {

    public static void main(String[] args) {
        User user = null;
        getLengthOfUsersName(user);
    }

    public static void getLengthOfUsersName(User user) {
        System.out.println("Length of first name: " + user.getName().getFirstName());
    }
}

class User {
    private Name name;
    private String email;

    public User(Name name, String email) {
        this.name = name;
        this.email = email;
    }

   //getter
   //setter
}

class Name {
    private String firstName;
    private String lastName;

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

   //getter
   //setter
}
Guarda il metodo di base in questo passaggio. Vediamo che successivamente verrà lanciata una NullPointerException . Se eseguiamo e compiliamo il codice in una versione precedente a Java 14, otterremo il seguente StackTrace:
Exception in thread "main" java.lang.NullPointerException
at com.bazlur.Main.getLengthOfUsersName(Main.java:11)
at com.bazlur.Main.main(Main.java:7)
Ci sono pochissime informazioni qui su dove e perché si è verificata la NullPointerException . Ma in Java 14 e versioni successive, otteniamo molte più informazioni in StackTrace, il che è molto comodo. In Java 14 vedremo:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "ca.bazlur.playground.User.getName()" because "user" is null
at ca.bazlur.playground.Main.getLengthOfUsersName(Main.java:12)
at ca.bazlur.playground.Main.main(Main.java:8)

8. CompletabileFuturo

Scriviamo i programmi riga per riga e solitamente vengono eseguiti riga per riga. Ma ci sono momenti in cui abbiamo bisogno dell'esecuzione parallela per rendere il programma più veloce. Per questo solitamente utilizziamo Java Thread. La programmazione dei thread Java non riguarda sempre la programmazione parallela. Ci dà invece la possibilità di comporre diversi moduli di programma indipendenti che verranno eseguiti in modo indipendente e spesso anche in modo asincrono. Tuttavia, la programmazione dei thread è piuttosto difficile, soprattutto per i principianti. Questo è il motivo per cui Java 8 offre un'API più semplice che consente di eseguire parte di un programma in modo asincrono. Vediamo un esempio. Diciamo che dobbiamo chiamare tre API REST e quindi combinare i risultati. Possiamo chiamarli uno per uno. Se ciascuno di essi impiega circa 200 millisecondi, il tempo totale per riceverli richiederà 600 millisecondi. E se potessimo eseguirli in parallelo? Poiché i processori moderni sono in genere multi-core, possono gestire facilmente tre chiamate di riposo su tre processori diversi. Usando CompletableFuture possiamo farlo facilmente.
package ca.bazlur.playground;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class SocialMediaService {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var service = new SocialMediaService();

        var start = Instant.now();
        var posts = service.fetchAllPost().get();
        var duration = Duration.between(start, Instant.now());

        System.out.println("Total time taken: " + duration.toMillis());
    }

    public CompletableFuture> fetchAllPost() {
        var facebook = CompletableFuture.supplyAsync(this::fetchPostFromFacebook);
        var linkedIn = CompletableFuture.supplyAsync(this::fetchPostFromLinkedIn);
        var twitter = CompletableFuture.supplyAsync(this::fetchPostFromTwitter);

        var futures = List.of(facebook, linkedIn, twitter);

        return CompletableFuture.allOf(futures.toArray(futures.toArray(new CompletableFuture[0])))
                .thenApply(future -> futures.stream()
                        .map(CompletableFuture::join)
                        .toList());
    }
    private String fetchPostFromTwitter() {
        sleep(200);
        return "Twitter";
    }

    private String fetchPostFromLinkedIn() {
        sleep(200);
        return "LinkedIn";
    }

    private String fetchPostFromFacebook() {
        sleep(200);
        return "Facebook";
    }

    private void sleep(int millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

9. Espressioni Lambda

Le espressioni Lambda sono forse la caratteristica più potente del linguaggio Java. Hanno cambiato il modo in cui scriviamo il codice. Un'espressione lambda è come una funzione anonima che può accettare argomenti e restituire un valore. Possiamo assegnare una funzione a una variabile e passarla come argomenti a un metodo e il metodo può restituirla. Ha un corpo. L'unica differenza rispetto al metodo è che non esiste un nome. Le espressioni sono brevi e concise. Di solito non contengono molto codice standard. Vediamo un esempio in cui dobbiamo elencare tutti i file presenti in una directory con estensione .java.
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
Se guardi attentamente questo pezzo di codice, abbiamo passato la classe interna anonima list() al metodo . E nella classe interna abbiamo inserito la logica per filtrare i file. Essenzialmente, siamo interessati a questa parte della logica, non allo schema attorno alla logica. L'espressione lambda ci permette di rimuovere l'intero template e possiamo scrivere il codice che ci interessa. Ecco un esempio:
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list((dir, name) -> name.endsWith(".java"));
Naturalmente questo è solo un esempio; le espressioni lambda hanno molti altri vantaggi.

10. API di flusso

Nel nostro lavoro quotidiano, uno dei compiti più comuni è l'elaborazione di un set di dati. Ha diverse operazioni comuni come filtraggio, trasformazione e raccolta di risultati. Prima di Java 8, tali operazioni erano di natura imperativa. Abbiamo dovuto scrivere il codice in base al nostro intento (ovvero, ciò che volevamo ottenere) e come vorremmo farlo. Con l'invenzione dell'espressione lambda e dell'API Stream, ora possiamo scrivere funzioni di elaborazione dati in modo dichiarativo. Indichiamo solo la nostra intenzione e non abbiamo bisogno di scrivere come otteniamo il risultato. Ecco un esempio: abbiamo un elenco di libri e vogliamo trovare tutti i nomi dei libri Java, separati da virgole e ordinati.
public static String getJavaBooks(List books) {
    return books.stream()
            .filter(book -> Objects.equals(book.language(), "Java"))
            .sorted(Comparator.comparing(Book::price))
            .map(Book::name)
            .collect(Collectors.joining(", "));
}
Il codice sopra è semplice, leggibile e conciso. Ma qui sotto puoi vedere un codice imperativo alternativo:
public static String getJavaBooksImperatively(List books) {
    var filteredBook = new ArrayList();
    for (Book book : books) {
        if (Objects.equals(book.language(), "Java")){
            filteredBook.add(book);
        }
    }
    filteredBook.sort(new Comparator() {
        @Override
        public int compare(Book o1, Book o2) {
            return Integer.compare(o1.price(), o2.price());
        }
    });

    var joiner = new StringJoiner(",");
    for (Book book : filteredBook) {
        joiner.add(book.name());
    }

    return joiner.toString();
}
Sebbene entrambi i metodi restituiscano lo stesso valore, possiamo vedere chiaramente la differenza.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION