2015-04-01 17 views
9

Durante la riproduzione in giro con il nuovo Java 8 Stream API ho avuto modo di chiedersi, perché non:Perché la mappa <K,V> non estende la funzione <K,V>?

public interface Map<K,V> extends Function<K, V> 

O anche:

public interface Map<K,V> extends Function<K, V>, Predicate<K> 

Sarebbe abbastanza facile da implementare con default metodi sul Mapinterface:

@Override default boolean test(K k) { 
    return containsKey(k); 
} 

@Override default V apply(K k) { 
    return get(k); 
} 

E sarebbe consentire l'uso di un Map in un metodo map:

final MyMagicMap<String, Integer> map = new MyMagicHashMap<>(); 
map.put("A", 1); 
map.put("B", 2); 
map.put("C", 3); 
map.put("D", 4); 

final Stream<String> strings = Arrays.stream(new String[]{"A", "B", "C", "D"}); 
final Stream<Integer> remapped = strings.map(map); 

O come un Predicate in un metodo filter.

Trovo che una parte significativa dei miei casi d'uso per un Map sia esattamente quel costrutto o uno simile - come rimappatura/ricerca Function.

Quindi, perché i progettisti JDK non hanno deciso di aggiungere questa funzionalità allo Map durante la riprogettazione per Java 8?

risposta

12

Il team JDK era certamente a conoscenza del rapporto matematico tra java.util.Map come una struttura di dati e java.util.function.Function come una funzione di mappatura. Dopotutto, Function è stato chiamato Mapper all'inizio del JDK 8 prototype builds. E l'operazione di streaming che chiama una funzione su ogni elemento del flusso è chiamata Stream.map.

C'è stata anche una discussione sull'eventuale ridenominazione di Stream.map in qualcos'altro come transform a causa di una possibile confusione tra una funzione di trasformazione e una struttura di dati Map. (Siamo spiacenti, non è possibile trovare un collegamento.) Questa proposta è stata respinta, con la logica che è la somiglianza concettuale (e che map per questo scopo è nell'uso comune).

La domanda principale è: cosa si otterrebbe se lo java.util.Map fosse un sottotipo di java.util.function.Function? C'è stata una discussione in comments sul fatto che la sottotipizzazione implichi una relazione "è-a". La sottotipizzazione è minore rispetto alle relazioni "is-a" degli oggetti - dal momento che stiamo parlando di interfacce, non di classi - ma implica la sostituibilità . Quindi, se Map erano un sottotipo di Function, uno sarebbe in grado di fare questo:

Map<K,V> m = ... ; 
source.stream().map(m).collect(...); 

Subito siamo confrontati con la cottura nel comportamento di quello che oggi è Function.apply a uno dei Map metodi esistenti. Probabilmente l'unica ragionevole è Map.get, che restituisce null se la chiave non è presente. Queste semantiche sono, francamente, un po 'schifose. applicazioni reali sono probabilmente andando ad avere per scrivere i propri metodi che forniscono la politica chiave mancante in ogni caso, così sembra che ci sia ben poco vantaggio di essere in grado di scrivere

map(m) 

invece di

map(m::get) 

o

map(x -> m.getOrDefault(x, def)) 
+2

Penso che il punto su cuocere in 'map :: get' piuttosto che in' getOrDefault' o 'computeIfAbsent' o quant'altro se l'argomento più convincente finora. Posso vedere che 'null' sarebbe un problema, specialmente visto che molti' Stream' inizieranno la vita come 'NONNULL'. Grazie per la tua preziosa visione della decisione di progettazione! –

4

Perché una mappa non è una funzione. L'ereditarietà è per A è una relazione B. Non per A può essere oggetto di vari tipi di relazioni B.

avere una funzione trasforma una chiave per il suo valore, basta

Function<K, V> f = map::get; 

Per avere un test predicato se un oggetto è contenuto in una mappa, basta

Predicate<Object> p = map::contains; 

Quello è sia più chiaro che più leggibile della tua proposta.

+1

Rovistando in Scala dimostra che la loro [ 'Map' fa mixin un' PartialFunction'] (http://www.scala-lang.org/api/2.11.4/index.html#scala.collection .MapLike). Suppongo che sia solo una diversa decisione progettuale, ma mostra che forse non è così chiaro come tutto ... –

+2

Come una mappa non è una funzione? Penso che sia perfettamente corretto definire una mappa come una funzione tale che 'f (k) = v' sul dominio definito dalle chiavi nella mappa e' f (x) = null' se x non è nel dominio. – user2336315

+0

@ user2336315 Una mappa non è una funzione in senso matematico. Chiamate multiple con la stessa chiave possono dare risultati diversi. – zeroflagL

8

La domanda è: “perché dovrebbe estendersi Function?”

Il tuo esempio di utilizzo strings.map(map) in realtà non giustifica l'idea di cambiare il tipo di ereditarietà (che implica l'aggiunta di metodi per l'interfaccia Map), data la poca differenza a strings.map(map::get). E non è chiaro se l'utilizzo di uno Map come Function è davvero così comune che dovrebbe ottenere quel trattamento speciale rispetto a, ad es. utilizzando map::remove come Function o utilizzando map::get di un Map<…,Integer> come ToIntFunction o map::get di un Map<T,T> come BinaryOperator.

Questo è ancora più discutibile nel caso di un Predicate; dovrebbe map::containsKey davvero ottenere un trattamento speciale rispetto a map::containsValue?

Vale anche la pena notare la firma del tipo dei metodi.Map.get ha una firma funzionale di Object → V mentre si suggerisce che Map<K,V> debba estendersi Function<K,V> che è comprensibile da una vista concettuale delle mappe (o semplicemente osservando il tipo), ma mostra che ci sono due aspettative contrastanti, a seconda che si guardi il metodo o al tipo. La soluzione migliore non è quella di correggere il tipo funzionale. Quindi è possibile assegnare a uno map::getFunction<Object,V> o Function<K,V> e tutti sono felici ...

+0

Penso che questo sia un argomento molto migliore di [risposta di JB Nizet] (http://stackoverflow.com/a/29388651/2071828), posso vedere la logica qui. Il punto chiave sembra essere che avere 'Mappa estende la Funzione ' causerebbe problemi con le primitive Java ('ToXXXFunction') e, cosa più importante, non funzionerebbe bene con la firma (un po 'hacky) di' get' - sono quelli le principali considerazioni? La risposta di base in questo caso sembra essere "problemi legacy" ... –

+1

@ Boris the Spider: non riesco a guardare nella mente degli sviluppatori. Forse in realtà non ci hanno mai pensato affatto. Soprattutto perché non vi è alcun tentativo di aggiornare a posteriori i tipi esistenti, ad es. lasciare che 'Comparator' estenda' ToIntBiFunction' ecc. Forse la risposta più semplice è che le funzioni e altri tipi sono considerati generalmente distinti. Avere 'Map' estendendo' Function' implica che potresti creare gemme come chiamare 'firstMap.compose (otherMap)' ... – Holger

+5

Incolparlo su problemi "legacy" è troppo facile. Holger ha ragione; la domanda giusta è "perché dovrebbe", piuttosto che "perché no?" E non c'era una ragione convincente perché dovrebbe, soprattutto considerando quanto sia semplice convertire map :: get o list :: contains to Function o Predicate. –

Problemi correlati