web-dev-qa-db-fra.com

Initialisation du champ paresseux avec lambdas

Je voudrais implémenter l'initialisation de champ paresseux (ou l'initialisation différée) sans une instruction if et en tirant parti de lambdas. Donc, j'aimerais avoir le même comportement de la propriété Foo suivante mais sans la if:

class A<T>{
    private T fooField;

    public T getFoo(){
        if( fooField == null ) fooField = expensiveInit();
        return fooField;
    }
}

Ignorer le fait que cette solution ne garantit pas une utilisation sûre pour: 1) le multi-threading; 2) null en tant que valeur valide de T.

Donc, pour exprimer l'intention de différer l'initialisation de fooField jusqu'à sa première utilisation, j'aimerais déclarer la fooField de type Supplier<T> telle que:

class A<T>{
   private Supplier<T> fooField = () -> expensiveInit();

   public T getFoo(){
      return fooField.get();
   }
}

puis dans la propriété getFoo, je renverrais simplement fooField.get(). Mais maintenant, je veux que les prochains appels à la propriété getFoo évitent la expensiveInit() et renvoient simplement l'instance précédente T.

Comment puis-je atteindre cet objectif sans utiliser une if?

Malgré les conventions de dénomination et le remplacement de ->by =>, cet exemple peut également être considéré en C #. Cependant, NET Framework version 4 fournit déjà un Lazy<T> avec la sémantique souhaitée.

39
rodolfino

Dans votre lambda actuel, vous pouvez simplement mettre à jour la fooField avec un nouveau lambda, tel que:

class A<T>{
    private Supplier<T> fooField = () -> {
       T val = expensiveInit();
       fooField = () -> val;
       return val;
    };

    public T getFoo(){
       return fooField.get();
    }
}

Encore une fois, cette solution n’est pas thread-safe comme le .Net Lazy<T> et ne garantit pas que les appels simultanés à la propriété getFoo retournent le même résultat.

32
Miguel Gamboa

En prenant la solution de Miguel Gamboa et en essayant de minimiser le code par champ sans sacrifier son élégance, je suis arrivé à la solution suivante:

interface Lazy<T> extends Supplier<T> {
    Supplier<T> init();
    public default T get() { return init().get(); }
}
static <U> Supplier<U> lazily(Lazy<U> lazy) { return lazy; }
static <T> Supplier<T> value(T value) { return ()->value; }

Supplier<Baz> fieldBaz = lazily(() -> fieldBaz=value(expensiveInitBaz()));
Supplier<Goo> fieldGoo = lazily(() -> fieldGoo=value(expensiveInitGoo()));
Supplier<Eep> fieldEep = lazily(() -> fieldEep=value(expensiveInitEep()));

Le code par champ n’est que légèrement plus grand que dans la solution Stuart Marks mais la propriété Nice de la solution originale est conservée: après la première requête, il n’y aura plus qu’une variable Supplier légère qui renvoie sans condition la valeur déjà calculée.

19
Holger

L’approche adoptée par La réponse de Miguel Gamboa est excellente:

private Supplier<T> fooField = () -> {
   T val = expensiveInit();
   fooField = () -> val;
   return val;
};

Cela fonctionne bien pour les champs paresseux uniques. Toutefois, si plusieurs champs doivent être initialisés de cette manière, il convient de copier et de modifier le passe-partout. Un autre champ devrait être initialisé comme ceci:

private Supplier<T> barField = () -> {
   T val = expensiveInitBar();          // << changed
   barField = () -> val;                // << changed
   return val;
};

Si vous pouvez supporter un appel de méthode supplémentaire par accès après l'initialisation, procédez comme suit. Premièrement, j'écrirais une fonction d'ordre supérieur qui renvoie une instance de fournisseur contenant la valeur mise en cache:

static <Z> Supplier<Z> lazily(Supplier<Z> supplier) {
    return new Supplier<Z>() {
        Z value; // = null
        @Override public Z get() {
            if (value == null)
                value = supplier.get();
            return value;
        }
    };
}

Une classe anonyme est appelée ici car elle a un état mutable, qui est la valeur mise en cache de la valeur initialisée.

Ensuite, il devient assez facile de créer de nombreux champs initialisés tardivement:

Supplier<Baz> fieldBaz = lazily(() -> expensiveInitBaz());
Supplier<Goo> fieldGoo = lazily(() -> expensiveInitGoo());
Supplier<Eep> fieldEep = lazily(() -> expensiveInitEep());

Note: Je vois dans la question qu'il stipule "sans utiliser une if". Il n’était pas clair pour moi si la préoccupation ici était d’éviter le temps d’exécution coûteux d’un if conditionnel (vraiment, c’est vraiment pas cher) ou s’il s’agissait plutôt d’éviter de répéter le if conditionnel à chaque passage. J'ai supposé que c'était le dernier cas et ma proposition répond à cette préoccupation. Si vous êtes préoccupé par la surcharge d'exécution d'une conditionnelle if, vous devez également tenir compte de la surcharge liée à l'invocation d'une expression lambda.

17
Stuart Marks

Project Lombok fournit une @Getter(lazy = true) annotation qui répond exactement à vos besoins.

10
Marcin Kłopotek

C'est soutenu,

En créant une petite interface et en combinant 2 nouvelles fonctionnalités introduites dans Java 8:

  • @FunctionalInterface annotation (permet d'affecter un lambda à une déclaration)
  • default mot-clé (définir une implémentation, comme une classe abstraite - mais dans une interface)

Il est possible d’obtenir le même comportement Lazy<T> que celui observé en C #.


Usage

Lazy<String> name = () -> "Java 8";
System.out.println(name.get());

Lazy.Java (copiez et collez cette interface dans un endroit accessible)

import Java.util.function.Supplier;

@FunctionalInterface
public interface Lazy<T> extends Supplier<T> {
    abstract class Cache {
        private volatile static Map<Integer, Object> instances = new HashMap<>();

        private static synchronized Object getInstance(int instanceId, Supplier<Object> create) {

            Object instance = instances.get(instanceId);
            if (instance == null) {
                synchronized (Cache.class) {
                    instance = instances.get(instanceId);
                    if (instance == null) {
                        instance = create.get();
                        instances.put(instanceId, instance);
                    }
                }
            }
            return instance;
        }
    }

    @Override
    default T get() {
        return (T) Cache.getInstance(this.hashCode(), () -> init());
    }

    T init();
}

Exemple en ligne - https://ideone.com/3b9alx

L'extrait suivant illustre le cycle de vie de cette classe d'assistance

static Lazy<String> name1 = () -> { 
    System.out.println("lazy init 1"); 
    return "name 1";
};

static Lazy<String> name2 = () -> { 
    System.out.println("lazy init 2"); 
    return "name 2";
};

public static void main (String[] args) throws Java.lang.Exception
{
    System.out.println("start"); 
    System.out.println(name1.get());
    System.out.println(name1.get());
    System.out.println(name2.get());
    System.out.println(name2.get());
    System.out.println("end"); 
}

va sortir

start
lazy init 1
name 1
name 1
lazy init 2
name 2
name 2
end

Voir la démo en ligne - https://ideone.com/3b9alx

3
Jossef Harush

Que dis-tu de ça? alors vous pouvez faire quelque chose comme ceci en utilisant LazyInitializer d'Apache Commons: https://commons.Apache.org/proper/commons-lang/javadocs/api-3.1/org/Apache/commons/lang3/concurrent/LazyInitializer. html

private static Lazy<Double> _lazyDouble = new Lazy<>(()->1.0);

class Lazy<T> extends LazyInitializer<T> {
    private Supplier<T> builder;

    public Lazy(Supplier<T> builder) {
        if (builder == null) throw new IllegalArgumentException();
        this.builder = builder;
    }
    @Override
    protected T initialize() throws ConcurrentException {
        return builder.get();
    }
}
2
Hidden Dragon

Voici un moyen qui fonctionne également si vous souhaitez passer des arguments (que vous n'avez pas lors de l'initialisation de l'interface fonctionnelle) à votre méthode expensiveInit.

public final class Cache<T> {
    private Function<Supplier<? extends T>, T> supplier;

    private Cache(){
        supplier = s -> {
            T value = s.get();
            supplier = n -> value;
            return value;
        };
    }   
    public static <T> Supplier<T> of(Supplier<? extends T> creater){
        Cache<T> c = new Cache<>();
        return () -> c.supplier.apply(creater);
    }
    public static <T, U> Function<U, T> of(Function<? super U, ? extends T> creater){
        Cache<T> c = new Cache<>();
        return u -> c.supplier.apply(() -> creater.apply(u));
    }
    public static <T, U, V> BiFunction<U, V, T> of(BiFunction<? super U, ? super V, ? extends T> creater){
        Cache<T> c = new Cache<>();
        return (u, v) -> c.supplier.apply(() -> creater.apply(u, v));
    }
}

L'utilisation est la même que Stuart Marks ' answer:

private final Function<Foo, Bar> lazyBar = Cache.of(this::expensiveBarForFoo);
1
Alex

Vous pouvez faire quelque chose dans ce sens: 

   private Supplier heavy = () -> createAndCacheHeavy();

   public Heavy getHeavy()
   {
      return heavy.get();
   }

   private synchronized Heavy createAndCacheHeavy()
   {
      class HeavyFactory implements Supplier
      {
         private final Heavy heavyInstance = new Heavy();

         public Heavy get()
         {
            return heavyInstance;
         }
      }

      if(!HeavyFactory.class.isInstance(heavy))
      {
         heavy = new HeavyFactory();
      }

      return heavy.get();
   }

J'ai récemment vu cela comme une idée de Venkat Subramaniam. J'ai copié le code de cette page .

L'idée de base est que le fournisseur, une fois appelé, se remplace par une implémentation d'usine plus simple qui renvoie l'instance initialisée.

C'était dans le contexte de l'initialisation paresseuse thread-safe d'un singleton, mais vous pouvez aussi l'appliquer à un champ normal, évidemment.

1
bowmore

Eh bien, je ne suggère pas vraiment de ne pas avoir de "si", mais voici mon point de vue sur le sujet:

Une méthode simple consiste à utiliser AtomicReference (l'opérateur ternaire ressemble toujours à un "if"):

private final AtomicReference<Something> lazyVal = new AtomicReference<>();

void foo(){
    final Something value = lazyVal.updateAndGet(x -> x != null ? x : expensiveCreate());
    //...
}

Mais il y a aussi toute la magie de la sécurité du fil dont on n'a peut-être pas besoin. Donc, je le ferais comme Miguel avec une petite tournure:

Etant donné que j'aime les one-liners simples, j'utilise simplement un opérateur ternaire (là encore, il se lit comme un "if"), mais je laissais l'ordre d'évaluation de Java faire sa magie pour définir le champ:

public static <T> Supplier<T> lazily(final Supplier<T> supplier) {
    return new Supplier<T>() {
        private T value;

        @Override
        public T get() {
            return value != null ? value : (value = supplier.get());
        }
    };
}

l'exemple de modification de champ de gerardw ci-dessus, qui fonctionne sans "si", peut également être simplifié. Nous n'avons pas besoin de l'interface. Nous avons juste besoin d'exploiter à nouveau "l'astuce" ci-dessus: le résultat d'un opérateur d'affectation est la valeur assignée, nous pouvons utiliser des crochets pour forcer l'ordre d'évaluation. Donc, avec la méthode ci-dessus, c'est juste:

Tout ce dont nous avons besoin est ceci:

static <T> Supplier<T> value(final T value) {
   return () -> value;
}


Supplier<Point> p2 = () -> (p2 = value(new Point())).get();
1
Brixomatic

Si vous avez besoin de quelque chose qui se rapproche du comportement de Lazy en C #, qui vous donne la sécurité des threads et vous garantisse que vous obtenez toujours la même valeur, il n'existe pas de moyen simple d'éviter if.

Vous devrez utiliser un champ volatile et une double vérification du verrouillage. Voici la version la plus basse d'empreinte mémoire d'une classe qui vous donne le comportement C #:

public abstract class Lazy<T> implements Supplier<T> {
    private enum Empty {Uninitialized}

    private volatile Object value = Empty.Uninitialized;

    protected abstract T init();

    @Override
    public T get() {
        if (value == Empty.Uninitialized) {
            synchronized (this) {
                if (value == Empty.Uninitialized) {
                    value = init();
                }
            }
        }
        return (T) value;
    }

}

Ce n'est pas si élégant à utiliser. Vous devez créer des valeurs paresseuses comme ceci:

final Supplier<Baz> someBaz = new Lazy<Baz>() {
    protected Baz init(){
        return expensiveInit();
    }
}

Vous pouvez gagner en élégance au prix d'une mémoire supplémentaire, en ajoutant une méthode d'usine comme celle-ci:

    public static <V> Lazy<V> lazy(Supplier<V> supplier) {
        return new Lazy<V>() {
            @Override
            protected V init() {
                return supplier.get();
            }
        };
    }

Maintenant, vous pouvez créer des valeurs paresseuses sécurisées pour les threads simplement comme ceci:

final Supplier<Foo> lazyFoo = lazy(() -> fooInit());
final Supplier<Bar> lazyBar = lazy(() -> barInit());
final Supplier<Baz> lazyBaz = lazy(() -> bazInit());
1
Phil S

La solution de Stuart Mark, avec une classe explicite. (Je pense que ce soit "meilleur" est une chose personnelle

public class ScriptTrial {

static class LazyGet<T>  implements Supplier<T> {
    private T value;
    private Supplier<T> supplier;
    public LazyGet(Supplier<T> supplier) {
        value = null;
        this.supplier = supplier;
    }

    @Override
    public T get() {
        if (value == null)
            value = supplier.get();
        return value;
    }

}

Supplier<Integer> lucky = new LazyGet<>(()->seven());

int seven( ) {
    return 7;
}

@Test
public void printSeven( ) {
    System.out.println(lucky.get());
    System.out.println(lucky.get());
}

}

0
gerardw

Que diriez-vous de cette . Certains switcheroos fonctionnels J8 pour éviter les ifs sur chaque accès .

import Java.util.function.Supplier;

public class Lazy<T> {
    private T obj;
    private Supplier<T> creator;
    private Supplier<T> fieldAccessor = () -> obj;
    private Supplier<T> initialGetter = () -> {
        obj = creator.get();
        creator = null;
        initialGetter = null;
        getter = fieldAccessor;
        return obj;
    };
    private Supplier<T> getter = initialGetter;

    public Lazy(Supplier<T> creator) {
        this.creator = creator;
    }

    public T get() {
        return getter.get();
    }

}
0
JasonW