[Java] Hoe wildcards koppelen?

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Infinitive
  • Registratie: Maart 2001
  • Laatst online: 25-09-2023
Ik wil een Map waarbij de key MyKey<T> en de value MyVal<T> heterogeen zijn in T. De definitie van MyKey en MyVal doen er niet zoveel toe, maar ter illustratie zou ik het volgende kunnen doen:
Java:
1
2
3
class MyKey<T> implements Comparable<MyKey<T>> { T obj; /* snip */ }
class MyVal<T> { T obj; }
Map<MyKey<?>,MyVal<?>> map;

Echter, dit is niet precies genoeg! Dat de wildcard een willekeurig type representeert, dat is precies wat ik hebben wil, maar ik wil ook vereisen dat de twee willekeurige typen hetzelfde zijn. Het volgende is nu niet mogelijk:
Java:
1
2
3
4
for (Map.Entry<MyKey<?>,MyVal<?>> e: map.entrySet())
  doSomething(e.getKey().obj, e.getVal().obj);

<T> void doSomething(T a, T b) { ... }

De twee wildcards worden geopend naar twee verschillende type constanten en doSomething vereist dat deze twee typen hetzelfde zijn.

Uiteraard zijn er wel mogelijkheden om om dit probleem heen te werken. Zo kan ik een kopie van de MyKey<T> opslaan in de MyVal<T>, of op de nodige plaatsen wat "unchecked" casts invoegen. Beide oplossingen zijn lelijk.

Ik zou dus eigenlijk iets willen uitdrukken met wat verzonnen syntax:
Java:
1
Map<exists T <MyKey<T>,MyVal<T>>>

Is iets dergelijk mogelijk in Java?

putStr $ map (x -> chr $ round $ 21/2 * x^3 - 92 * x^2 + 503/2 * x - 105) [1..4]


Acties:
  • 0 Henk 'm!

  • momania
  • Registratie: Mei 2000
  • Laatst online: 18:15

momania

iPhone 30! Bam!

Dit zou kunenn werken

Java:
1
Map<MyKey<? extends ObjecX>,MyVal<? extends ObjectX>> map;


Mij ontgaat het idee een beetje waarom je dit met een map wil doen. Kan je niet beter een eigen value object maken?

Java:
1
2
3
4
5
6
7
8
9
10
class MyObject<T,T> {
  private final T x;
  private final T y;

  public MyObject(T x, T y) {
    this.x = x;
    this.y = y;
  }
  // etc
}


En die dan gewoon in een collection stoppen?

Neem je whisky mee, is het te weinig... *zucht*


Acties:
  • 0 Henk 'm!

  • Marcj
  • Registratie: November 2000
  • Laatst online: 16:21
Je kunt zoiets doen:

Java:
1
2
3
public static <T> Map<MyKey<T>, MyValue<T>> createMap() {
    return new HashMap<MyKey<T>, MyValue<T>>();
}


Maar die T moet ergens van tevoren gedefinieerd worden. En wat hebben die MyKey en MyValue dan nog voor nut? Waarin wil je het gebruiken?

edit:

Wacht, je wil dus eigenlijk niet weten welk type jouw map heeft, maar wel de eis kunnen neerleggen dat ze hetzelfde zijn? Dan nog moet je al eerder die T gaan definieren. Bijvoorbeeld door die for-loop ook in een klasse of methode te stoppen waar T gedefinieerd wordt. Anders is dit nooit type-safe.
Wordt die map aan een functie meegegeven? Of is het een field in een klasse?

[ Voor 40% gewijzigd door Marcj op 27-02-2009 08:06 ]


Acties:
  • 0 Henk 'm!

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

Infinitive schreef op vrijdag 27 februari 2009 @ 04:03:
Is iets dergelijk mogelijk in Java?
Je kan in ieder geval nog een eigen Map maken ala:

class MyMap<T> extends (Hash)Map<MyKey<T>, MyVal<T>>

Maar dat is best veel werk om alle relevante calls te overerven en ze onbewerkt dan door te geven, hoewel het wel als voordeel heeft dat je die complete MyKey/MyVal-laag transparant kan maken.

Afgezien daarvan zal je op een ander punt toch ergens die typen moeten definieren zoals Marcj al zegt.

Als je een willekeurige Map binnenkrijgt en daarvan domweg niet weet hoe of wat, dan kan je nog Collections.checkedMap gebruiken om wel een geldige Map te krijgen.

Acties:
  • 0 Henk 'm!

  • Infinitive
  • Registratie: Maart 2001
  • Laatst online: 25-09-2023
Marcj schreef op vrijdag 27 februari 2009 @ 08:02:
Wacht, je wil dus eigenlijk niet weten welk type jouw map heeft, maar wel de eis kunnen neerleggen dat ze hetzelfde zijn?
Precies. Ik wil dat je bij een insert het type kan kiezen en dat dit niet voor elke entry hetzelfde hoeft te zijn (vandaar: hetrogeen). Wanneer je er een waarde uithaalt, dan weet je niet wat de T was geweest. Maar in mijn geval hoef ik dat ook niet te weten. Ze moeten alleen gelijk zijn. Misschien denk je dat je niets kan doen als je het type niet weet, maar dat is niet waar. In het voorbeeld stopte ik bijvoorbeeld objecten van het type MyKey<T> en MyVal<T> in de map. Stel dat MyKey nog een paar extra methoden heeft die een T als parameter verwachten. Ook al weet je niet wat nu precies die T was, het feit dat je weet dat ze hetzelfde zijn is voldoende om die methode te kunnen aanroepen met de T-waarde in MyVal<T>.
momania schreef op vrijdag 27 februari 2009 @ 07:39:
Dit zou kunenn werken

Java:
1
Map<MyKey<? extends ObjecX>,MyVal<? extends ObjectX>> map;
Helaas, dat dwingt niet af dat de typen waarmee MyKey en MyVal geparametriseerd zijn, hetzelfde moeten zijn.
Mij ontgaat het idee een beetje waarom je dit met een map wil doen. Kan je niet beter een eigen value object maken?
Ik gebruik de map zodat ik voor een MyKey<T> snel een MyVal<T> kan opzoeken. Hoewel je op zich gelijk hebt: ik kan de MyKey<T> ook als een field in MyVal<T> opnemen. Maar dan ben ik feitelijk data aan het dupliceren, met als probleem dat je niet moet vergeten dit field dan op het juiste moment van de juiste waarde te voorzien. Goed, dit is dusdanig simpel dat het geen groot probleem is, maar erg netjes is het niet ;)
ACM schreef op vrijdag 27 februari 2009 @ 08:24:
Je kan in ieder geval nog een eigen Map maken ala:

Java:
1
class MyMap<T> extends (Hash)Map<MyKey<T>, MyVal<T>>
Mja, dat lijkt me wel een beetje overkill ;)
Maar je hebt gelijk: ben niet direct volledig overtuigt dat dit werkt, maar anders is het wel een antwoord op de vraag die ik stelde :)
[edit] Helaas: Je kunt op deze manier de "put" methode niet aanroepen (van de super-class)

[ Voor 43% gewijzigd door Infinitive op 27-02-2009 19:00 ]

putStr $ map (x -> chr $ round $ 21/2 * x^3 - 92 * x^2 + 503/2 * x - 105) [1..4]


Acties:
  • 0 Henk 'm!

  • ACM
  • Registratie: Januari 2000
  • Niet online

ACM

Software Architect

Werkt hier

Ik zou geen manier weten om dat per entry los af te dwingen dat je dezelfde types er in zet, maar dat het je verder niet uitmaakt of die van de eerste entry anders is dan van de tweede entry.

Sterker nog, het hele generics-gebeuren gaat overboord na de compilatie en jij wilt pas run-time het type van de key en value kunnen specificeren. Het lijkt me dat generics dan niet heel erg bruikbaar is voor wat jij wilt en dat je eerder met reflection en dergelijke aan de slag moet en in ieder geval niet van de standaard Map's van Java gebruik kan maken.

Acties:
  • 0 Henk 'm!

  • Mr_Light
  • Registratie: Maart 2006
  • Niet online

Mr_Light

Zo-i-Zo de gekste.

Entries kunnen dus verschillende typen hebben maar binnen een entry wil je wel afdwingen dat de key het zelfde type heeft als de value.

Het implementen van Map interface gaat name clashes opleveren vanwege type erasure.
Verder is het lastig putAll(Map) op een fijne manier te implementeren, entrySet() lijkt me ook vervelend.

Maar als je niet Map implementeerd en dus niet een echte een decorator maakt kan je wel wat voor elkaar krijgen als ik je vereisten goed snap.

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SpecialMap<BaseType> {
    
    private final Map<BaseType, BaseType> decoratedMap;

    public SpecialMap(Map<BaseType, BaseType> decoratedMap) {
        this.decoratedMap = decoratedMap;
    }
    
    ...

    @SuppressWarnings("unchecked") // should be save because put is protected.
    public <Foo extends BaseType> Foo get(Foo key) {
        return (Foo) decoratedMap.get(key);
    }

    @SuppressWarnings("unchecked") // should be save because put is protected.
    public <Foo extends BaseType> BaseType put(Foo key, Foo value) {
        return (Foo) decoratedMap.put(key, value);
    }

    @SuppressWarnings("unchecked") // should be save because put is protected.
    public <Foo> Foo remove(Foo key) {
        return (Foo) decoratedMap.remove(key);
    }

    ... 
}
Ik gebruik de map zodat ik voor een MyKey<T> snel een MyVal<T> kan opzoeken.
Beetje slecht voorbeeld in je openings post dan - daar itereer je door alle entries van de map. Wat even snel zou zijn als alle elementen in een collectie.

Bovenstaande werkt in deze context ook niet omdat je niet tijdends compile time weet wat er in runtime in de map zit. (Mijn glazen bol is stuk iig)

Ik ben trouwens wel errug benieuwd wat dat doSomething() doet. (Ik bedoel dit voldoet ook aan je eisen:
Java:
1
2
3
4
for (Map.Entry<MyKey<?>,MyVal<?>> e: map.entrySet())
  doSomething((Object)e.getKey().obj, (Object)e.getVal().obj);

<T> void doSomething(T a, T b) { ... }

Meuh altijd bijde het zelfde type: Object 8)7

Als de upperbound lowerbound moet zijn dan geld dat voor alle objecten in de map en kan je casten naar je lowere upperbound... :z

IceManX schreef: sowieso


Acties:
  • 0 Henk 'm!

  • Infinitive
  • Registratie: Maart 2001
  • Laatst online: 25-09-2023
Mijn voorbeeld in de startpost was wellicht iets te veel vereenvoudigd. Laat ik het iets concreter maken. Ik merk hier bij op dat het nog steeds om een verzonnen versimpeling van mijn daadwerkelijke toepassing gaat.

Stel ik heb de volgende wrapper voor pointers in Java:
Java:
1
class Pointer<T> { T val; }

Een pointer naar een string kan nu bijvoorbeeld gewrapped worden door een Pointer<String> en een pointer naar een Integer met een Pointer<Integer>.

Stel dat ik nu een historie van waarden bijhoud van waarden die zo'n pointer bevatte:
Java:
1
Map<Pointer<T>, Stack<T>> history;

probleem 1: Dit is een declaratie van een homogene map. Hierin kun je alleen maar pointers van hetzelfde type opslaan. Ofwel, ik wil een hetrogene map.

Vervolgens wil ik een waarde aan dit history kunnen toevoegen:
Java:
1
2
3
4
5
<T> void add(Pointer<T> ptr, T val)
{
    Stack<T> s = history.get(ptr);
    s.push(val);
}

probleem 2: Als history een hetrogene map is d.m.v. Map<Pointer<?>,Stack<?>>, dan krijg je hier niet afgedwongen dat de Stack<T> geparametriseerd is met dezelfde T als Pointer<T>.

Nu wil je al je pointers een waarde in history terug zetten:
Java:
1
2
3
4
5
void revert()
{
    for (Map.Entry<Pointer<?>,Stack<?>> e : history.getEntrySet())
        e.getKey().val = e.getValue().pop();
}

Probleem 3: Voor de toekenning moeten de twee wildcards hetzelfde zijn. Het is niet van belang of dit nu een String of een Integer of een ander type is; ze hoeven alleen maar hetzelfde te zijn. Maar dat is nu juist wat hier niet afgedwongen wordt.

Mijn huidige oplossing is om gedeeltelijk van de generics af te stappen door als als history een Map<Pointer<Object>,Stack<Object>> te nemen en op diverse plaatsen casts te plaatsen en bepaalde compiler-warnings te onderdrukken. Het idee achter generics is dat casts overbodig worden als je statisch kan aantonen dat het object al van het juiste type is. Dat is hier het geval: een Pointer<T> is altijd geassocieerd met een Stack<T>.

Ik hoop dus dat het mogelijk is om een dergelijke invariant uit te drukken, maar ik vrees dat het generics-mechanisme daar niet krachtig genoeg voor is.

putStr $ map (x -> chr $ round $ 21/2 * x^3 - 92 * x^2 + 503/2 * x - 105) [1..4]


Acties:
  • 0 Henk 'm!

  • Mr_Light
  • Registratie: Maart 2006
  • Niet online

Mr_Light

Zo-i-Zo de gekste.

Je collectie heeft een type voor alle waardes als je alle waardes, als je waardes toevoegd met verschillende lowerbound types(ergens omhoog moeten ze het zelfde type hebben anders kunnen ze niet toegevoegd worden) dan verlies je dus de informatie/kennis van van wat het lowerbound type van het object is. Je zal ergens anders die informatie moeten opslaan als je het later weer wilt weten(proben). (Het verplaatsen van het probleem van de value naar de key lost niet veel op.)

Volgens mij gaat wat je wilt niet eens lukken zonder Type erasure laat staan met. Maar volgens mij was je daar al zelf achter:
probleem 1: Dit is een declaratie van een homogene map. Hierin kun je alleen maar pointers van hetzelfde type opslaan. Ofwel, ik wil een hetrogene map.
Je wilt dus van te voren declareren wat de typen zijn die in de map komen voordat je weet welke typen er in komen. Als je het anders bedoeld en ik begrijp je verkeerd is je eerste veronderstelling niet goed: het is geen hetrogene map het enige wat afgedwongen word is dat Objecten in de collectie een bepaald basis type delen. De enige manier hoe je dat soort dingen afdwingt is een sandbox te bouwen waardoor je voorkomt dat het later fout gaat en dus die toekomstige handelingen veilig zijn: ala bounds checking, je kan nooit een waarde welke niet bij de array hoort terug krijgen(buffer overflow) omdat je niet waardes buiten je bounds kan opvragen, omdat het opvragen resulteerd in een ArrayOutOfBoundsException. Je sluit het systeem gedeeltelijk waardoor mogelijk onveilige operaties veilig worden, omdat onveilige paden uitgesloten zijn. Zoals hier boven - omdat je bij put afdwingt dat je dezelfde Type gebruikt word voor de key en de value word de cast veilig. - Die cast had niet gehoeven als de compiler zo 'slim' was dat hij dat kon zien(nu escaped de boel sowieso bij de constructor, dus zo veilig is het nu ook weer niet..)

Wat is er mis met een Lazily created stack aan je Pointer te hangen?

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Pointer<T> {
 
 protected Pointer() {..}

 Stack getHistory() {..}

 void setValue(T value) {
  <create stack if needed>
  add old value 
  set new one
 } 
 
 void revert() {...}

}

class PointerFactory {
  // een weak reference collectie..

 Pointer<T> createPointer(T value) {
  <create pointer>
  <add to weak reference collectie>
  return pointer;
 }

 void revertHistory {
  for(Pointer<?> value : weakCollection) {
   value.revert();
  }
 }
}

Volgens mij moet je dan ook nog een Factory per scope hebben (dus een andere naam is wellicht beter.) hmm, trouwens als je echt terug wilt kunnen zal het mischien niet een weak reference moeten zijn. trouwens volgens mij kan de ene pointer vaker van waarde veranderen dan de ander dus over alle waardes itereren gaat nooit goed en zal je ook die historie ergens bij moeten houden om daadwerkelijk te kunnen reverten. Het simpelweg in een 'platte' collectie stoppen gaat dan ook niet werken.

IceManX schreef: sowieso


Acties:
  • 0 Henk 'm!

  • Confusion
  • Registratie: April 2001
  • Laatst online: 01-03-2024

Confusion

Fallen from grace

Wat misschien een praktisch haalbare oplossing is, is om alles toch niet in dezelfde heterogene map te stoppen, maar er een map tussen te stoppen waarmee je bij diverse homogene maps terechtkomt, door eerst een lookup op de runtime class te doen:
Java:
1
Map<Class<T>, Map<Pointer<T>, Stack<T>>>

Wie trösten wir uns, die Mörder aller Mörder?


Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Ik denk dat Mr_Light het bij het rechte eind heeft. Java is hier niet de beperkende taal, jij wil gewoon een construct die vanuit design-perspectieven niet helemaal klopt.

Waarom heeft een value een externe map nodig om zijn eigen history bij te houden? Mij lijkt het dat beide strongly coupled zijn.

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
class History<T>
{
  Stack<T> history;
  void revert()
  {
    history.pop();
  }  
  T getValue()
  {
    return history.top();
  } 
}
Set<History<?>> myValues;


Bemerk dat je de current value en de history helemaal niet apart hoeft bij te houden.
De laatste value is altijd de top-of-stack.

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

  • Infinitive
  • Registratie: Maart 2001
  • Laatst online: 25-09-2023
H!GHGuY schreef op zaterdag 28 februari 2009 @ 10:29:
Waarom heeft een value een externe map nodig om zijn eigen history bij te houden?
Dit was slechts een voorbeeld. In mijn echte situatie representeren de "pointers" een globale structuur, en zijn er verschillende maps met lokale meta data.

Ik heb er nog wat langer over nagedacht. In theorie zou je met een ander soort Map een eind kunnen komen. Maar wat je ook probeert, zonder casts kan je het volgende niet uitdrukken:
Java:
1
2
3
4
5
6
7
8
9
10
11
class Entry<T> { Pointer<T> key; T val; }
    
class MyMap {
    private Set<Entry<?>> set;

    <T> T add(Pointer<T> ptr, T val) { ... }
        
    <T> T get(Pointer<T> ptr) {
        for (Entry<?> e : set) {
            if (e.key == ptr) {
                return e.val;


In de foreach loop is het type waarmee de Entry geparametriseerd wordt onbekend. Java introduceert hiervoor een vers type capture#xxx (laten we dit C noemen) waarvan alleen aangenomen mag worden dat het Object als supertype heeft. Ofwel, Java meldt terecht dat e.val van het type C is en dat is ongelijk aan T.
Maar... dat is niet waar. Omdat e.key gelijk moet zijn aan ptr weet je dat Pointer<C> gelijk moet zijn aan Pointer<T>, en dat als gevolg daarvan C en T gelijk moeten zijn. Dit is een statische eigenschap wat geen runtime type informatie nodig heeft. Java ondersteund deze vorm van type refinement echter niet.

Nu is het wellicht ook wel te verwachten dat een dergelijke feature niet ondersteund wordt omdat een if-then-else te ingewikkeld is om statisch te bepalen welke extra type-aannamen er in scope zijn. Uiteraard kan je daar een envoudiger constructie voor introduceren. Sommige talen hebben dat in de vorm van Generalized Algebraic Data Types. Ook voor object-georienteerde talen is dit mogelijk.

De praktische oplossing is dus om de (unchecked) casts voor lief te nemen (statisch is gegarandeerd dat deze altijd zullen slagen).

putStr $ map (x -> chr $ round $ 21/2 * x^3 - 92 * x^2 + 503/2 * x - 105) [1..4]


Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Wat denk je dan van:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Data
{
   private Map<String, Object> MetaData;
   
   void attachMetaData(String id, Object o) { ... }
   void deleteMetaData(String id) { ... }
   void getMetaData(String id) { ... }
}

class MetaData1 { }

void HandleData(Data d)
{
  MetaData1 md = (MetaData1)d.getMetaData("MetaData1");
  ...
}

Aangepast naar jouw noden natuurlijk.

Ik denk dat je beter niet naar een workaround voor de taal kunt zoeken, maar naar een mooi design wat je wel in die taal kan uitdrukken.

ASSUME makes an ASS out of U and ME

Pagina: 1