Generics in Java per Android: guida completa con esempi pratici
I Generics in Java sono uno degli strumenti più potenti per scrivere codice Android riutilizzabile, sicuro e facile da mantenere. In questo articolo vediamo come usarli in modo pratico in un progetto Android, dalla sintassi base fino a una mini app che utilizza un adapter generico per visualizzare sia prodotti che utenti in una RecyclerView Android.
1. Introduzione: cos’è un Generic e perché usarlo in Android
Un Generic in Java è un modo per scrivere classi e metodi che lavorano con tipi di dato parametrici, ovvero tipi che puoi specificare dall’esterno (come String, Integer, una tua classe Product, ecc.). Il vantaggio principale è che puoi riutilizzare lo stesso codice per tipi diversi mantenendo il type safety, cioè il controllo dei tipi a compile-time.
In Android questo è particolarmente utile per:
- adapter riutilizzabili per RecyclerView Android
- classi di risposta API generiche (ad esempio risultati di Retrofit)
- repository generici per accedere ai dati (API, database locale, cache)
- utility riutilizzabili per la gestione di risultati (success/failure) e validazioni
Se hai già dimestichezza con la Programmazione ad Oggetti in Android, i Generics sono il passo successivo naturale per rendere il tuo codice più pulito.
2. Sintassi base: <T>, <T extends …>, wildcard <?>
La sintassi dei Generics ruota tutta intorno alle parentesi angolari <>. Vediamo i casi più comuni.
<T>: tipo generico di base
T è il nome convenzionale per un tipo generico. Potresti usare qualsiasi nome (es. E, U, R), ma T è lo standard. Esempio:
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// Uso
Box<String> stringBox = new Box<>("Ciao");
Box<Integer> intBox = new Box<>(10);
<T extends …>: limiti sul tipo (bounded types)
Se vuoi che il parametro generico sia solo di un certo tipo o sottotipo, usi <T extends MyClass>. Ad esempio, vuoi una lista che accetti solo elementi che implementano Parcelable (utile in Android per passare dati tra Activity, come visto in Richiamare un Activity Android):
public class ParcelableBox<T extends android.os.Parcelable> {
private T value;
public ParcelableBox(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Wildcard <?>: tipo sconosciuto
La wildcard <?> rappresenta un tipo sconosciuto. È utile quando vuoi accettare una collezione generica senza specificare esattamente il tipo:
public void stampaLista(List<?> items) {
for (Object item : items) {
Log.d("Lista", item.toString());
}
}
Più avanti, nella sezione sugli errori comuni, vedremo anche <? extends T> e <? super T>.
3. Classe generica: esempio con ApiResponse<T> (success, data, message)
Un esempio tipico in Android è incapsulare la risposta di una chiamata API in una classe generica. Invece di avere una classe per ogni tipo di risposta, usi ApiResponse<T>:
public class ApiResponse<T> {
private boolean success;
private T data;
private String message;
public ApiResponse(boolean success, T data, String message) {
this.success = success;
this.data = data;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public T getData() {
return data;
}
public String getMessage() {
return message;
}
}
// Esempi di utilizzo
ApiResponse<List<Product>> productsResponse;
ApiResponse<User> userResponse;
ApiResponse<Void> logoutResponse; // Nessun dato, solo successo/messaggio
In questo modo puoi definire una sola volta la logica di gestione della risposta (successo/errore) e riutilizzarla per qualsiasi tipo di dato.
4. Metodo generico: esempio pratico
Oltre alle classi, anche i metodi possono essere generici. Un caso utile in Android è un metodo utility che converte una Response<T> di Retrofit nella nostra ApiResponse<T> generica.
public class ApiUtils {
// <T> definisce il tipo generico del metodo
public static <T> ApiResponse<T> fromRetrofitResponse(retrofit2.Response<T> response) {
if (response.isSuccessful()) {
return new ApiResponse<>(
true,
response.body(),
"OK"
);
} else {
return new ApiResponse<>(
false,
null,
"Errore HTTP: " + response.code()
);
}
}
}
Ora puoi usare lo stesso metodo per qualsiasi chiamata Retrofit, indipendentemente dal tipo di dato restituito.
5. Adapter generico per RecyclerView con <T>
Gli adapter della RecyclerView Android sono un candidato perfetto per i Generics. L’idea è creare un adapter riutilizzabile che non dipenda da un tipo specifico (es. Product), ma lavori con un <T> generico e deleghi il binding al chiamante tramite un callback.
public class GenericAdapter<T> extends RecyclerView.Adapter<GenericAdapter.GenericViewHolder> {
public interface Binder<T> {
void onBind(View itemView, T item, int position);
}
private List<T> items;
private int layoutId;
private Binder<T> binder;
public GenericAdapter(List<T> items, int layoutId, Binder<T> binder) {
this.items = items;
this.layoutId = layoutId;
this.binder = binder;
}
@NonNull
@Override
public GenericViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(layoutId, parent, false);
return new GenericViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull GenericViewHolder holder, int position) {
T item = items.get(position);
binder.onBind(holder.itemView, item, position);
}
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}
public static class GenericViewHolder extends RecyclerView.ViewHolder {
public GenericViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}
Questo adapter non conosce il tipo concreto degli oggetti, ma si limita a passarlo al callback. Sarà il codice dell’Activity o del Fragment a dire come effettuare il binding.
6. Repository generico: BaseRepository<T>
Un Repository in architetture come MVVM serve a incapsulare l’accesso ai dati (API, database, cache, ecc.). Un BaseRepository<T> può contenere logica comune per gestire errori, caching o mapping delle risposte, mentre i repository specifici ereditano da esso.
public abstract class BaseRepository<T> {
protected MutableLiveData<ApiResponse<T>> liveData = new MutableLiveData<>();
public LiveData<ApiResponse<T>> getLiveData() {
return liveData;
}
protected <R> void handleCall(retrofit2.Call<R> call, Function<R, T> mapper) {
call.enqueue(new Callback<R>() {
@Override
public void onResponse(Call<R> call, Response<R> response) {
ApiResponse<R> apiResponse = ApiUtils.fromRetrofitResponse(response);
if (apiResponse.isSuccess() && apiResponse.getData() != null) {
T mapped = mapper.apply(apiResponse.getData());
liveData.postValue(new ApiResponse<>(true, mapped, "OK"));
} else {
liveData.postValue(new ApiResponse<>(false, null, apiResponse.getMessage()));
}
}
@Override
public void onFailure(Call<R> call, Throwable t) {
liveData.postValue(new ApiResponse<>(false, null, t.getMessage()));
}
});
}
}
Un repository concreto per i prodotti potrebbe estendere BaseRepository<List<Product>> e usare handleCall() per centralizzare la gestione della chiamata.
7. Bounded type parameters: <T extends Comparable<T>>
Un caso tipico di bounded type è quando vuoi lavorare solo con tipi che possono essere confrontati, ovvero che implementano Comparable. In Android questo è utile, ad esempio, per ordinare liste prima di mostrarle in una RecyclerView.
public class SortUtils {
public static <T extends Comparable<T>> void sortList(List<T> list) {
Collections.sort(list);
}
}
// Uso
List<Integer> numbers = Arrays.asList(3, 1, 2);
SortUtils.sortList(numbers);
List<String> names = Arrays.asList("Luca", "Anna", "Marco");
SortUtils.sortList(names);
Se vuoi ordinare una tua classe (es. Product), ti basta implementare Comparable<Product> nella classe e puoi riutilizzare lo stesso metodo.
8. Mini app completa: lista generica di Prodotti e Utenti con lo stesso adapter
Vediamo ora un esempio completo in cui usiamo lo stesso GenericAdapter<T> per mostrare sia una lista di Product che una lista di User. Supponiamo di avere un layout di riga semplice con un TextView.
// Product.java
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
// User.java
public class User {
private String username;
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
}
Layout di item (res/layout/item_simple.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/textTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
Activity con due RecyclerView che usano lo stesso adapter generico:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerProducts;
private RecyclerView recyclerUsers;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerProducts = findViewById(R.id.recyclerProducts);
recyclerUsers = findViewById(R.id.recyclerUsers);
recyclerProducts.setLayoutManager(new LinearLayoutManager(this));
recyclerUsers.setLayoutManager(new LinearLayoutManager(this));
List<Product> products = Arrays.asList(
new Product("Mouse", 19.99),
new Product("Tastiera", 29.99)
);
List<User> users = Arrays.asList(
new User("luca", "luca@example.com"),
new User("anna", "anna@example.com")
);
GenericAdapter<Product> productAdapter = new GenericAdapter<>(
products,
R.layout.item_simple,
(itemView, product, position) -> {
TextView title = itemView.findViewById(R.id.textTitle);
TextView subtitle = itemView.findViewById(R.id.textSubtitle);
title.setText(product.getName());
subtitle.setText(String.format(Locale.getDefault(), "%.2f €", product.getPrice()));
}
);
GenericAdapter<User> userAdapter = new GenericAdapter<>(
users,
R.layout.item_simple,
(itemView, user, position) -> {
TextView title = itemView.findViewById(R.id.textTitle);
TextView subtitle = itemView.findViewById(R.id.textSubtitle);
title.setText(user.getUsername());
subtitle.setText(user.getEmail());
}
);
recyclerProducts.setAdapter(productAdapter);
recyclerUsers.setAdapter(userAdapter);
}
}
Con questo approccio hai scritto un solo adapter e lo stai usando per due tipi completamente diversi. Puoi facilmente estendere l’idea per gestire il click sugli elementi, ad esempio aprendo un DialogFragment Android o una nuova Activity (vedi Richiamare un Activity Android).
9. Errori comuni con i Generics
Lavorando con i Generics è facile incorrere in alcuni errori tipici. Eccone alcuni da tenere a mente nello sviluppo Android.
- Raw types (es. List invece di List<T>): usare il tipo grezzo disabilita il controllo dei tipi e può portare a
ClassCastExceptiona runtime. Evitalo sempre, specifica sempre il tipo generico. - Erasure dei tipi: a runtime le informazioni sui generics vengono rimosse (type erasure). Questo significa che non puoi fare cose come
new T()oinstanceof List<String>in modo diretto. Spesso devi passare esplicitamente laClass<T>se ti serve a runtime. - Confusione tra <? extends T> e <? super T>: una regola utile è PECS (Producer Extends, Consumer Super). Usa
<? extends T>quando la collezione produce valori (la leggi), e<? super T>quando la collezione consuma valori (ci scrivi dentro). - Metodi troppo generici: inserire troppi parametri generici nello stesso metodo o classe può rendere il codice difficile da leggere. Mantieni i Generics dove hanno un reale beneficio di riuso e chiarezza.
- Cast non sicuri: se ti ritrovi spesso a fare cast espliciti all’interno di classi generiche, forse la tua modellazione non è ideale. Cerca di sfruttare il type system per evitare i cast.
10. Conclusione e link agli articoli correlati
I Generics in Java sono fondamentali per scrivere codice Android moderno, pulito e riutilizzabile. Dalle API ai repository, dagli adapter per RecyclerView alle utility di sorting, imparare a usarli bene ti aiuterà a ridurre la duplicazione e a prevenire molti errori di tipo.
Per approfondire concetti collegati e vedere altri esempi pratici, puoi leggere anche:
- Programmazione ad Oggetti in Android
- RecyclerView Android
- Richiamare un Activity Android
- DialogFragment Android
Integrare i Generics nei tuoi prossimi progetti Android ti permetterà di costruire una base solida e facilmente estendibile per le tue app.
Lascia un commento