我编写了这个类,可以使用构建器模式构建类型为T的数组,将值存储在闭包中,直到实际构造数组。
public class ArrayBuilder<T> {
final Class<T> type;
public ArrayBuilder(Class<T> type){
this.type = type;
}
private Supplier<Supplier<Store>> start = () -> {
final Store element = new Store(-1, null, null);
return () -> element;
};
private class Store {
final Integer index;
final T val;
final Supplier<Store> getNextVal;
public Store(Integer index, T val, Supplier<Store> getNextVal) {
this.index = index;
this.val = val;
this.getNextVal = getNextVal;
}
}
private Supplier<Store> queue(Integer index, T value, Supplier<Store> next) {
final Store element = new Store(index, value, next);
return () -> element;
}
public ArrayBuilder<T> add(T element) {
Supplier<Store> currentStore = start.get();
Integer currentIndex = start.get().get().index + 1;
start = () -> queue(currentIndex, element, currentStore);
return this;
}
public T[] build() {
Store nextVal = start.get().get();
Integer size = nextVal.index + 1;
T[] result = makeGenericArray(size);
while (nextVal.index != -1) {
result[nextVal.index] = nextVal.val;
nextVal = nextVal.getNextVal.get();
}
return result;
}
private T[] makeGenericArray(Integer size) {
return (T[]) Array.newInstance(type, size);
}
}
这很有效,但是我想知道在调用build()之前存储值的位置(堆栈?,堆?)?是否有任何理由不应该使用或执行?它确实使用了反射,但只有在调用build()时才支付成本。
答案 0 :(得分:5)
嗯,确切地说,堆和堆栈都涉及lambda / closure构造过程。要构建闭包的思维模型,您可以将其视为为每个lambda事件创建一个类的实例,并将所有来自lambda访问的父作用域的变量传递给该类的构造函数。但是,让我们尝试通过一个示例来了解为构建lambda的闭包时JVM究竟做了什么:
public void performLambdasDemo() {
// Declare variables which are going to be used in the lambda closure
final Pair methodPairIntegerValue = new Pair(RandomUtils.nextInt(), RandomUtils.nextInt());
final Integer methodIntegerValue = RandomUtils.nextInt();
// Declare lambda
final Supplier methodSupplierLambda = () -> {
return methodPairIntegerValue.fst + 9000 + methodIntegerValue.intValue();
};
// Declare anonymous class
final Supplier methodSupplierInnerClass = new Supplier() {
@Override
public Integer get() {
return methodPairIntegerValue.fst + 9001 + methodIntegerValue.intValue();
}
};
System.out.println(methodSupplierLambda.get());
System.out.println(methodSupplierInnerClass.get());
}
这个无用的代码所做的实际上是构建一个lambda和匿名内部类的实例完全相同。现在让我们来看看两者的相应字节代码。
<强> lambda表达式强>
下面是为lambda生成的字节码:
L2
LINENUMBER 35 L2
ALOAD 1
ALOAD 2
INVOKEDYNAMIC get(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/util/function/Supplier; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
()Ljava/lang/Object;,
// handle kind 0x6 : INVOKESTATIC
com/sfl/stackoverflow/LambdasExperiment.lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;,
()Ljava/lang/Integer;
]
ASTORE 3
L3
// Omit quite some byte-code and jump to the method declaration
// access flags 0x100A
private static synthetic lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;
L0
LINENUMBER 36 L0
ALOAD 0
GETFIELD com/sun/tools/javac/util/Pair.fst : Ljava/lang/Object;
CHECKCAST java/lang/Integer
INVOKEVIRTUAL java/lang/Integer.intValue ()I
SIPUSH 9000
IADD
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
IADD
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ARETURN
MAXSTACK = 2
MAXLOCALS = 2
尽管用Java字节码编写,但上面的代码完全是自我解释:
ALOAD 1 ALOAD 2
这两个命令将引用methodPairIntegerValue
和methodIntegerValue
推送到堆栈(这里是堆栈部分进来)。接下来是INVOKEDYNAMIC
命令。此命令是来自匿名内部类的lambda的主要区别因素。如果对于匿名内部类,在字节代码中生成一个显式的新类,对于lambdas,实际的实现被推迟到应用程序的运行时。但是,大多数现代JVM在发现INVOKEDYNAMIC
时会生成一个新类,该类具有两个属性,捕获在INVOKEDYNAMIC
之前推送到堆栈的值并创建它的新实例(这里有额外的堆使用跳转) )。值得一提的是,这些操作不是由INVOKEDYNAMIC
直接执行,而是由调用委托给的LambdaMetafactory
执行。因此,结束输出与匿名内部类非常相似(JVM可以在将来自由更改由LambdaMetafactory
合并的此实现细节)。
private static synthetic lambda$performLambdasDemo$1(Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)Ljava/lang/Integer;
这是一个包含lambda表达式实际代码的静态方法。它将在LambdaMetafactory
调用期间由类INVOKEDYNAMIC
调用。正如您所看到的那样,从堆栈中提取2个值并执行实际求和。
匿名课程
Bellow是使用匿名类的字节码,这里事情比较简单,因此我只添加了匿名类的启动部分,省略了实际类的字节码:
L3
LINENUMBER 39 L3
NEW com/sfl/stackoverflow/LambdasExperiment$2
DUP
ALOAD 0
ALOAD 1
ALOAD 2
INVOKESPECIAL com/sfl/stackoverflow/LambdasExperiment$2. (Lcom/sfl/stackoverflow/LambdasExperiment;Lcom/sun/tools/javac/util/Pair;Ljava/lang/Integer;)V
ASTORE 4
L4
代码的作用是将this
,methodPairIntegerValue
,methodIntegerValue
的值推送到堆栈并调用匿名类的构造函数,该类在匿名字段中捕获这些值类。
从上面的代码片段可以看出,内存占用明智的lambdas和匿名内部类非常相似。
<强>摘要强>
回到你的问题: 闭包中使用的引用使用堆栈传递。 生成的匿名类的实例及其包含闭包中使用的变量的引用的字段存储在堆中(如果您明确使用类而不是lambda并传递通过构造函数的值)。
然而,关于引导过程和JIT,lambdas和匿名内部类的性能存在一些差异。以下链接详细介绍了该主题:
希望这有帮助(尽管答案有点冗长)