Jak ręcznie zainicjalizować pole parametryzowanego typu wewnątrz generycznej klasy?

0

Chcę stworzyć generator IDków, który może być sparametryzowany np. Longiem lub Integerem.

final class IdGenerator<ID extends Number> {
    private ID id;

    IdGenerator() {
        System.out.println(this.id instanceof Long);
        System.out.println(this.id instanceof Integer);
        if (id instanceof Long) {
            id = (ID) Long.valueOf(0L);
        } else if (id instanceof Integer) {
            id = (ID) Integer.valueOf(0);
        } else {
            throw new RuntimeException("Invalid ID type (must be Long or Integer)");
        }
    }

    ID nextId() {
        if (id instanceof Long) {
            id = (ID) Long.valueOf(id.longValue() + 1L);
        } else if (id instanceof Integer) {
            id = (ID) Integer.valueOf(id.intValue() + 1);
        } else {
            throw new RuntimeException("Invalid ID type (must be Long or Integer)");
        }
        return id;
    }

    public static void main(String[] args) {
        IdGenerator<Integer> longIdGenerator = new IdGenerator<>();
        Integer id = longIdGenerator.nextId();
    }
}

Po uruchomieniu tego kodu wywala wyjątek w konstruktorze, bo id jest nullem więc instanceof zwraca false.

IdGenerator siedzi w innej generycznej klasie, a na szczycie hierarchii wygląda to tak:

public class InMemoryBookRepository extends InMemoryCrudRepository<BookEntity, Long> implements BookRepository {
}

Zatem odpada:

  • przekazanie argumentu określającego typ (np. Long.class)
  • przekazanie do konstruktora początkowej wartości

Cały problem sprowadza się do tego, żeby w konstruktorze jakoś zainicjalizować pole id odpowiednią wartością.

Wiem, że w runtime nie ma informacji o typie, jakim została sparametryzowana klasa, da się to jakoś "obejść"?

1
Potat0x napisał(a):

Wiem, że w runtime nie ma informacji o typie, jakim została sparametryzowana klasa, da się to jakoś "obejść"?

Jeżeli bardzo chcesz to zawsze jest co najmniej jeden sposób: wstrzyknij Class<ID> przez konstruktor.

Możesz też zrobić metodę fabrykującą IdGenerator.forType(Class) która zwraca podklasę IdGenerator w zależności od przekazanej klasy.

Potat0x napisał(a):

Zatem odpada:

  • przekazanie argumentu określającego typ (np. Long.class)

Aaa, nie doczytałem wcześniej. Dlaczego odpada? W którymś miejscu i tak konkretyzujesz generyka, więc zamiast CośTam<Integer>(...) możesz tam wstawić CośTam<>(Integer.class, ...) itp itd a potem przepychać tę instancję klasy Class dalej.

2

Mniej więcej wiem co chcesz zrobić, ale niestety w Javie IMO nie da się tego zrobić prosto i elegancko.

Twój kod z instanceof to już w ogóle dramat: jeśli masz serie instanceof to wiedz, że coś się dzieje.

IMO zrób IDGenerator, który zawsze longa generuje. Wywal inty :-), są dla słabiaków.

1

Ten kod wygląda na potężnego raka :D Ja bym jednak dał argumenty do konstruktora, najlepiej T initialValue oraz Function<T,T> incrementor. Przeciez tam gdzie tworzysz ten obiekt wiesz już jaki typ chcesz mieć. Zresztą taki interfejs jest dużo bardziej użyteczny, bo pozwala zacząć nie tylko od 0 i generować inne sekwencje niż tylko +1.
Od biedy zawsze możesz też zrobić sobie jakieś Factory które będzie zwracać kilka takich "standardowych" Generatorów.

1

<ID extends Number> - Ile takich generatorów będziesz miał w praktyce? ;-) Warto iść w generyki dla takich przypadków? Może jeden wystarczy z nextInt()/nextLong() ?
Wygląda jakbyś chciał generować unikalne klucze, dlaczego nie UUID ?

2

Kilka lat temu tworzyłem podobne twory typu generyczne DAO z generykami. I tak budowałem na tym złożoną hierarchię innych klas, po czym doszedłem do ściany. Nie pomagały extendsy, ani supery. Rozwiązywałem zły problem. W końcu skończyło się na rawach. Generalnie wyszło beznadziejnie. Wujek dobra rada: przemyśl design swojego kodu, może lepiej zejść z generyków.

0

Miałem takie zamierzenie, żeby wszystko zaczynało się w klasie, jaką podałem w pierwszym poście (bo podobnie się robi w Spring Data JPA)

public class InMemoryBookRepository extends InMemoryCrudRepository<BookEntity, Long> implements BookRepository {
}

Mało kodu, tylko definicja klasy. Całą magię chciałem ukryć pod spodem, żeby użytkownik nie musiał już niczego robić.

Dalej to wygląda tak:

public interface BookRepository extends CrudRepository<BookEntity, Long> {
}
public class InMemoryCrudRepository<T, ID extends Number> implements CrudRepository<T, ID> {

    private final Map<ID, T> repository = new HashMap<>();
    private final IdGenerator<ID> idGenerator = new IdGenerator<>();
    private final EntityRipper<T, ID> entityRipper = new EntityRipper<>();
    
   //tutaj implementacje metod
}

Mam świadomość, że instanceof to rak, po prostu jak mam Number będący Longiem albo Integerem, to chciałem użyć odpowiedniej metody. Na tak niskim pozimie chyba nie zawsze da się robić rzeczy "ładnie"? :P (to nie kod produkcyjny, próbuję napisać narzędzie ułatwiające testy)

Wygląda jakbyś chciał generować unikalne klucze, dlaczego nie UUID ?

To ma działać dla typowej encji JPA (która już została napisana), w której ID jest liczbą.

w Javie IMO nie da się tego zrobić prosto i elegancko

To mi nie przeszkadza (chyba, że będzie trzeba operować bytecode).

Ciężka sprawa. Spróbuję zastosować się do porady @Charles_Ray, zobaczymy co z tego wyjdzie :)

2

To najbliżej Javy da się (chyba!) to zrobić w kotlinie dzięki reified generics.
Chyba, bo na 100% pewny nie jestem. Reified generics mają swoje ograniczenia. Używam czasem do podobnie rakowych rzeczy :-).

Kotlin będzie miał tą zaletę, że mimo że nie ma typeclass - to jednak minimalne zmiany w buildzie i ekosystemie masz - kilka linijek w pom lub gradle. Możesz miksować pliki javowe z kotlinowymi bez strat właściwie.

Jeśli to do testów to szczególnie tego kłótlina polecam. JUnit obsysa.

3

Wrzucam potworka jako ciekawostkę :)

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;

abstract class AbstractGenericClass<K extends Number> {

    AbstractGenericClass() {
        var typeOfK = DontDoThisAtHome.seriously(getClass(), AbstractGenericClass.class, "K");
        System.out.println("K is " + typeOfK.getSimpleName() + " for " + getClass().getSimpleName());
    }
}

class A extends AbstractGenericClass<Long> {}

abstract class B<A extends Number> extends AbstractGenericClass<A> {}

class B_1 extends B<Double> {}

class B_2 extends B<Integer> {}


class DontDoThisAtHome {

    @SuppressWarnings("rawtypes")
    static Class<?> seriously(Class<?> instanceClass, Class<?> typeDeclarationClass, String typeName) {
        var typeParameters = Arrays.asList(typeDeclarationClass.getTypeParameters());
        var typeVariableDeclaration = typeParameters.stream()
            .filter(variable -> typeName.equals(variable.getName()))
            .findFirst()
            .orElseThrow();
        var typeVariableIndex = typeParameters.indexOf(typeVariableDeclaration);
        var typeDefinition = Stream.iterate((Class) instanceClass, Class::getSuperclass)
            .map(Class::getGenericSuperclass)
            .takeWhile(Objects::nonNull)
            .filter(ParameterizedType.class::isInstance)
            .map(ParameterizedType.class::cast)
            .filter(type -> typeDeclarationClass.equals(type.getRawType()))
            .map(type -> type.getActualTypeArguments()[typeVariableIndex])
            .findFirst()
            .orElseThrow();
        if (typeDefinition instanceof Class<?>) {
            return (Class<?>) typeDefinition;
        } else if (typeDefinition instanceof TypeVariable<?>) {
            var variableTypeDefinition = (TypeVariable<?>) typeDefinition;
            var variableDeclarationType = (Class<?>) variableTypeDefinition.getGenericDeclaration();
            return seriously(instanceClass, variableDeclarationType, variableTypeDefinition.getName());
        } else {
            throw new IllegalStateException("kek");
        }
    }
}

public class Main {

    public static void main(String[] args) {
	    new A();
	    new B_1();
	    new B_2();
    }
}
0

Problem rozwiązany, nie musiałem rezygnować z generyków. Zrezygnowałem za to z nadmiernego upraszczania definicji repozytoriów i zrobiłem tak jak napisał @Shalom:

public final class InMemoryBookRepository extends InMemoryCrudRepository<BookEntity, Long> implements BookRepository {
    public InMemoryBookRepository() {
        super(0L, IdGenerators.IncrementalLongIdGenerator);
        //super(123L, id -> id + 10);
    }
}

Dzięki temu kod został odrakowany i teraz można bez problemu użyć nawet UUID (jak ktoś chce to Long też może być :D)

@damianem
Kompletnie nie czaję co tam się dzieje, szanuję :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1