开销:原始流与装箱之间的转换
Overhead: Convert Between Primitive Streams vs. Boxing
我正在使用 Java 8 流 API,如下所示:
private Function<Long, Float> process; // Intermediate step (& types)
private long getWeekFrequency(final ScheduleWeek week) {
return week.doStreamStuff().count(); // Stream<>.count() returns long
}
@Override
public float analyse(final Schedule sample) {
return (float) sample // cast back to float
.getWeeks()
.stream()
.mapToLong(this::getWeekFrequency) // object to long
.mapToDouble(process::apply) // widen float to double
.sum();
}
@Override
public String explain(final Schedule sample) {
return sample
.getWeeks()
.stream()
.map(this::getWeekFrequency) // change stream type?
.map(String::valueOf)
.collect(Collectors.joining(", "));
}
问题
我假设在 object/primitive 流类型之间切换时会产生开销...如果我坚持使用 Stream<>,这与装箱开销相比如何?
如果我以后换回来呢?
具体来说:
在分析师中,我应该使用.map(...).mapToDouble(...)
吗?
在解释中,我应该使用 .mapToLong(...).mapToObj(...)
?
所以让我们分解一下:
.mapToLong(this::getWeekFrequency)
给你一个原始长。
.mapToDouble(process::apply)
这个原始 long 被装箱到 Long
因为 process
函数需要它。 process
returns 一个 Float
映射到原始双精度数(通过 Float.doubleValue()
)。
对这些求和并将总和转换为原始浮点数(变窄,但你说安全),然后将其返回。
那么我们怎样才能摆脱一些自动装箱呢?我们想要一个 FunctionalInterface
与我们的 process
函数完全匹配,而不使用任何框 类。没有一个我们可以使用现成的,但我们可以很容易地这样定义它:
@FunctionalInterface
public interface LongToFloatFunction
{
float apply(long l);
}
然后我们将声明更改为:
private LongToFloatFunction process;
并保持其他一切不变,这将防止任何自动装箱。函数返回的原始 float 将自动加宽为原始 double。
好吧,根据你的定义,process
看起来有点像这样:
double process (long value) {
// do something
}
因此,如果您这样做:map(...).mapToDouble
您每次都会创建一个 Long
类型的对象,只是在 process
中使用后立即将其拆箱。我会保留代码,因为它是使用可以避免这种情况的原始实现。
第二个使用String#valueOf
。在 long
的情况下,将调用 String.valueOf(l)
,它适用于原语:Long.toString(l)
.
在 Object
的情况下将调用相同的方法, 但 需要注意的是第一次装箱发生。所以,我会把它改成 mapToLong
我正在使用 Java 8 流 API,如下所示:
private Function<Long, Float> process; // Intermediate step (& types)
private long getWeekFrequency(final ScheduleWeek week) {
return week.doStreamStuff().count(); // Stream<>.count() returns long
}
@Override
public float analyse(final Schedule sample) {
return (float) sample // cast back to float
.getWeeks()
.stream()
.mapToLong(this::getWeekFrequency) // object to long
.mapToDouble(process::apply) // widen float to double
.sum();
}
@Override
public String explain(final Schedule sample) {
return sample
.getWeeks()
.stream()
.map(this::getWeekFrequency) // change stream type?
.map(String::valueOf)
.collect(Collectors.joining(", "));
}
问题
我假设在 object/primitive 流类型之间切换时会产生开销...如果我坚持使用 Stream<>,这与装箱开销相比如何?
如果我以后换回来呢?
具体来说:
在分析师中,我应该使用.map(...).mapToDouble(...)
吗?
在解释中,我应该使用 .mapToLong(...).mapToObj(...)
?
所以让我们分解一下:
.mapToLong(this::getWeekFrequency)
给你一个原始长。
.mapToDouble(process::apply)
这个原始 long 被装箱到 Long
因为 process
函数需要它。 process
returns 一个 Float
映射到原始双精度数(通过 Float.doubleValue()
)。
对这些求和并将总和转换为原始浮点数(变窄,但你说安全),然后将其返回。
那么我们怎样才能摆脱一些自动装箱呢?我们想要一个 FunctionalInterface
与我们的 process
函数完全匹配,而不使用任何框 类。没有一个我们可以使用现成的,但我们可以很容易地这样定义它:
@FunctionalInterface
public interface LongToFloatFunction
{
float apply(long l);
}
然后我们将声明更改为:
private LongToFloatFunction process;
并保持其他一切不变,这将防止任何自动装箱。函数返回的原始 float 将自动加宽为原始 double。
好吧,根据你的定义,process
看起来有点像这样:
double process (long value) {
// do something
}
因此,如果您这样做:map(...).mapToDouble
您每次都会创建一个 Long
类型的对象,只是在 process
中使用后立即将其拆箱。我会保留代码,因为它是使用可以避免这种情况的原始实现。
第二个使用String#valueOf
。在 long
的情况下,将调用 String.valueOf(l)
,它适用于原语:Long.toString(l)
.
在 Object
的情况下将调用相同的方法, 但 需要注意的是第一次装箱发生。所以,我会把它改成 mapToLong