Limite superiore del tipo restituito generico - interfaccia vs. classe - codice sorprendentemente valido


171

Questo è un esempio reale di un'API di librerie di terze parti, ma semplificato.

Compilato con Oracle JDK 8u72

Considera questi due metodi:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Entrambi riportano un avviso "non controllato" - capisco perché. La cosa che mi sconcerta è perché posso chiamare

Integer x = getCharSequence();

e si compila? Il compilatore dovrebbe sapere che Integernon implementa CharSequence. La chiamata a

Integer y = getString();

dà un errore (come previsto)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Qualcuno può spiegare perché questo comportamento dovrebbe essere considerato valido? Come sarebbe utile?

Il client non sa che questa chiamata non è sicura: il codice del client viene compilato senza preavviso. Perché la compilazione non dovrebbe avvisare di ciò / emettere un errore?

Inoltre, in cosa differisce da questo esempio:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Tentare di passare List<Integer>genera un errore, come previsto:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Se questo viene segnalato come errore, perché Integer x = getCharSequence();non lo è?


15
interessante! il casting su LHS Integer x = getCharSequence();verrà compilato, ma il casting su RHS Integer x = (Integer) getCharSequence();non verrà compilato
fiocchi

Quale versione del compilatore Java stai usando? Si prega di specificare queste informazioni nella domanda.
Federico Peralta Schaffner,

@FedericoPeraltaSchaffner non riesce a capire perché sia ​​importante: questa è una domanda direttamente su JLS.
Boris the Spider,

@BoristheSpider Perché il meccanismo di inferenza del tipo è cambiato per java8
Federico Peralta Schaffner

1
@FedericoPeraltaSchaffner - Ho già taggato la domanda con [java-8], ma ora ho aggiunto la versione del compilatore nel post.
Adam Michalik,

Risposte:


184

CharSequenceè un interface. Pertanto, anche se SomeClassnon implementato CharSequence, sarebbe perfettamente possibile creare una classe

class SubClass extends SomeClass implements CharSequence

Quindi puoi scrivere

SomeClass c = getCharSequence();

perché il tipo inferito Xè il tipo di intersezione SomeClass & CharSequence.

Questo è un po 'strano nel caso di Integerperché Integerè definitivo, ma finalnon gioca alcun ruolo in queste regole. Ad esempio puoi scrivere

<T extends Integer & CharSequence>

D'altra parte, Stringnon è un interface, quindi sarebbe impossibile estenderlo SomeClassper ottenere un sottotipo di String, perché java non supporta l'ereditarietà multipla per le classi.

Con l' Listesempio, è necessario ricordare che i generici non sono né covarianti né contraddittori. Ciò significa che se Xè un sottotipo di Y, List<X>non è né un sottotipo né un supertipo di List<Y>. Poiché Integernon implementa CharSequence, non è possibile utilizzare List<Integer>nel doCharSequencemetodo.

Tuttavia, puoi farlo compilare

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Se hai un metodo che restituisce un List<T>simile a questo:

static <T extends CharSequence> List<T> foo() 

tu puoi fare

List<? extends Integer> list = foo();

Ancora una volta, questo è perché il tipo inferito è Integer & CharSequencee questo è un sottotipo di Integer.

I tipi di intersezione si verificano implicitamente quando si specificano più limiti (ad es <T extends SomeClass & CharSequence>.).

Per ulteriori informazioni, ecco la parte di JLS in cui spiega come funzionano i limiti di tipo. È possibile includere più interfacce, ad es

<T extends String & CharSequence & List & Comparator>

ma solo il primo limite può essere una non interfaccia.


62
Non avevo idea che potresti inserire una &nella definizione generica. +1
fiocchi

13
@flkes Puoi metterne più di uno, ma solo il primo argomento può essere una non interfaccia. <T extends String & List & Comparator>va bene ma <T extends String & Integer>non lo è, perché Integernon è un'interfaccia.
Paul Boddington,

7
@PaulBoddington Vi è un certo uso pratico di questi metodi. Ad esempio se il tipo non è effettivamente utilizzato per i dati memorizzati. Esempi per questo sono Collections.emptyList()pure Optional.empty(). Queste restituiscono implementazioni di un'interfaccia generica, ma non memorizzano nulla.
Stefan Dollase,

6
E nessuno dice che una classe in fase finaldi compilazione sarà finalin fase di esecuzione.
Holger,

7
@Federico Peralta Schaffner: il punto qui è, il metodo getCharSequence()promette di restituire qualunque cosa Xil chiamante abbia bisogno, che include la restituzione di un tipo che si estende Integere l'implementazione CharSequencese il chiamante ne ha bisogno e, sotto questa promessa, è corretto consentire l'assegnazione del risultato a Integer. È il metodo getCharSequence()che è rotto in quanto non mantiene le promesse, ma non è colpa del compilatore.
Holger,

59

Il tipo inferito dal compilatore prima dell'assegnazione per Xè Integer & CharSequence. Questo tipo sembra strano, perché Integerè definitivo, ma è un tipo perfettamente valido in Java. Viene quindi lanciato Integer, il che è perfettamente OK.

V'è esattamente un possibile valore per il Integer & CharSequencetipo: null. Con la seguente implementazione:

<X extends CharSequence> X getCharSequence() {
    return null;
}

Il seguente incarico funzionerà:

Integer x = getCharSequence();

A causa di questo possibile valore, non c'è motivo per cui l'assegnazione debba essere sbagliata, anche se ovviamente è inutile. Un avvertimento sarebbe utile.

Il vero problema è l'API, non il sito di chiamata

In effetti, ho recentemente scritto un blog su questo anti pattern di progettazione API . Non dovresti (quasi) mai progettare un metodo generico per restituire tipi arbitrari perché non puoi (quasi) mai garantire che il tipo inferito verrà consegnato. Un'eccezione sono metodi come Collections.emptyList(), nel caso in cui il vuoto dell'elenco (e la cancellazione di tipo generico) sia il motivo per cui qualsiasi inferenza <T>funzionerà:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.