使用 JavaFX + Reactor 绘制函数图像并数值求解交点 —— 原理与实现
使用 JavaFX Reactor 绘制函数图像并数值求解交点 —— 原理与实现在数值计算与可视化中我们常常需要绘制函数曲线并标注其交点。本文将以一个完整的 Java 程序为例深入剖析如何利用Reactor生成数据点、JavaFX绘制曲线并通过纯数值方法符号检测 二分法求解交点最终在图上以空心圆圈和坐标标签的形式清晰显示。代码全程不依赖任何数学求导公式完全基于离散点采样与迭代逼近具有通用性强、易于扩展的特点。一、程序概览我们定义两个函数( f1(x) 3x^2 - 1 )二次函数( f2(x) 2x - 1 )一次函数目标是绘制它们在区间 ([-5, 5]) 上的图像并求出所有交点坐标在图上用空心圆标注。二、数据生成Reactor 的Flux流2.1 为何使用 ReactorReactor 提供了一套响应式编程 API其Flux可以方便地生成大量离散数据点并以声明式的方式进行处理。与传统 for 循环相比Flux更易于组合、并行处理和链式操作。2.2 代码解析doublestep0.01;doublestartX-5.0;doubleendX5.0;intcount(int)((endX-startX)/step)1;Flux.range(0,count).map(i-startXi*step).doOnNext(x-{series1.getData().add(newXYChart.Data(x,f1(x)));series2.getData().add(newXYChart.Data(x,f2(x)));}).blockLast();Flux.range(0, count)生成一个整数序列 0 到 count-1。map(i - startX i * step)将每个索引映射为对应的 ( x ) 值从而得到均匀分布的离散点。doOnNext是副作用操作在每次发出 ( x ) 时计算两个函数值并添加到 JavaFX 的Series数据列表中。blockLast()阻塞当前线程直到所有数据生成完毕。由于数据量不大约 1000 点该操作对 UI 线程影响很小。这样我们便得到了绘制曲线所需的所有点。三、绘图JavaFX 的LineChart3.1 坐标轴设置NumberAxisxAxisnewNumberAxis();xAxis.setLowerBound(-5);xAxis.setUpperBound(5);xAxis.setAutoRanging(false);固定 x 轴范围确保定义域完整显示。y 轴则自动适应数据范围。3.2 创建图表与系列LineChartNumber,NumberchartnewLineChart(xAxis,yAxis);XYChart.SeriesNumber,Numberseries1newXYChart.Series();series1.setName(f1(x) 3x² - 1);LineChart会按照数据点顺序连接成折线由于步长足够小0.01曲线将非常平滑。setCreateSymbols(false)禁用默认数据点符号避免干扰。3.3 添加数据在doOnNext中我们直接将计算出的数据点加入series的getData().add()该方法会在 JavaFX 应用程序线程中更新但blockLast()会阻塞 UI 线程直到所有点添加完毕这个过程只持续几十毫秒用户无感知。四、数值求解交点4.1 原理两个连续函数的交点满足 ( f_1(x) f_2(x) )等价于差值函数 ( h(x) f_1(x) - f_2(x) ) 的零点。若 ( h(x) ) 在某区间两端异号则根据介值定理该区间内必存在至少一个根。4.2 实现步骤离散采样在定义域内以步长step采样计算每个点的 ( h(x) )。符号检测遍历相邻点若h1 * h2 0则说明该区间内存在根。二分法精化对每个含根区间使用二分法Bisection Method迭代求根直到区间宽度小于给定精度1e-10。处理端点若某点 ( h(x) ) 恰好为零或绝对值极小直接视为交点。privateListdouble[]findIntersections(doublestep,doublestartX,doubleendX){Listdouble[]resultnewArrayList();intcount(int)((endX-startX)/step)1;double[]xValsnewdouble[count];double[]hValsnewdouble[count];for(inti0;icount;i){doublexstartXi*step;xVals[i]x;hVals[i]f1(x)-f2(x);}for(inti0;icount-1;i){if(hVals[i]*hVals[i1]0){doublerootbisect(xVals[i],xVals[i1]);doubleyf1(root);result.add(newdouble[]{root,y});}if(Math.abs(hVals[i])1e-12){doubleyf1(xVals[i]);result.add(newdouble[]{xVals[i],y});}}// 检查最后一点if(Math.abs(hVals[count-1])1e-12){doubleyf1(xVals[count-1]);result.add(newdouble[]{xVals[count-1],y});}returnresult;}4.3 二分法细节privatedoublebisect(doublea,doubleb){doubleeps1e-10;doublemid;intmaxIter100;for(inti0;imaxIter;i){mid(ab)/2;doublefaf1(a)-f2(a);doublefmf1(mid)-f2(mid);if(Math.abs(fm)eps)returnmid;if(fa*fm0)bmid;elseamid;}return(ab)/2;}二分法保证收敛且精度可控。对于本例只需几次迭代即可达到1e-10精度。五、交点标注自定义节点5.1 需求使用空心圆圈标记交点位置。在圆圈上方显示坐标标签避免遮挡曲线。5.2 实现为每个交点数据创建一个XYChart.Data并通过setNode设置自定义的StackPane。XYChart.DataNumber,NumberdatanewXYChart.Data(p[0],p[1]);StackPanenodenewStackPane();CirclecirclenewCircle(6);circle.setFill(null);// 空心circle.setStroke(Color.RED);circle.setStrokeWidth(2);LabellabelnewLabel(String.format((%.2f, %.2f),p[0],p[1]));label.setTextFill(Color.BLUE);label.setTranslateY(-20);// 上移node.getChildren().addAll(circle,label);data.setNode(node);5.3 为什么放在最上层先添加曲线系列series1, series2再添加交点系列使得交点绘制在曲线之上避免被覆盖。六、为什么数值解与解析解一致令 ( f_1(x) f_2(x) )[3x^2 - 1 2x - 1 \Rightarrow 3x^2 - 2x 0 \Rightarrow x0 \text{ 或 } x\frac{2}{3}]对应 y 值为 -1 和 1/3。数值解在1e-10精度下完全吻合验证了算法的正确性。这证明了在步长足够小、二分法精度足够高的情况下数值方法能可靠地逼近解析解。七、与之前学习内容的联系Context/ContextView虽未直接用于此程序但理解上下文传递有助于在更复杂的响应式流中传递调试信息。SynchronousSinkFlux.generate曾用于创建无限流而本例使用Flux.range生成有限点集两者都是 Reactor 生成数据的常用方式。Fuseable内部优化对于大量数据点可提升性能但本例数据量不大未显式使用。数值求交延续了之前对数函数导数、三角数流等数值计算的思想体现了响应式流在科学计算中的实用性。八、运行与效果运行程序后窗口显示两条平滑曲线交点处出现红色空心圆圈上方附有蓝色坐标标签清晰无遮挡。控制台无额外输出全部信息可视化。九、总结本文通过一个完整的 Java 程序展示了如何结合Reactor和JavaFX响应式生成大量离散点以绘制平滑曲线。利用数值差分法符号检测 二分法求解函数交点。自定义图表节点实现空心圆圈与标签标注。该方案不依赖解析公式可推广至任意连续函数具有广泛的应用价值。掌握这些技术将为后续的数值分析与可视化开发奠定坚实基础。完整代码publicclassFunctionPlotterextendsApplication{// 定义函数导数 f1(x) 3x² - 1privatedoublef1(doublex){return3*x*x-1;}// 定义函数直线 f2(x) 2x - 1privatedoublef2(doublex){return2*x-1;}Overridepublicvoidstart(StageprimaryStage){// 坐标轴NumberAxisxAxisnewNumberAxis();xAxis.setLabel(x);xAxis.setLowerBound(-5);xAxis.setUpperBound(5);xAxis.setAutoRanging(false);NumberAxisyAxisnewNumberAxis();yAxis.setLabel(y);yAxis.setAutoRanging(true);// 图表LineChartNumber,NumberchartnewLineChart(xAxis,yAxis);chart.setTitle(导数函数与直线交点);chart.setCreateSymbols(false);// 不显示数据点默认符号避免干扰doublestep0.01;doublestartX-5.0;doubleendX5.0;intcount(int)((endX-startX)/step)1;// 系列1f1(x)XYChart.SeriesNumber,Numberseries1newXYChart.Series();series1.setName(f1(x) 3x² - 1);// 系列2f2(x)XYChart.SeriesNumber,Numberseries2newXYChart.Series();series2.setName(f2(x) 2x - 1);// 使用 Reactor 生成数据点Flux.range(0,count).map(i-startXi*step).doOnNext(x-{series1.getData().add(newXYChart.Data(x,f1(x)));series2.getData().add(newXYChart.Data(x,f2(x)));}).blockLast();// ---- 数值求交点 ----Listdouble[]intersectionsfindIntersections(step,startX,endX);// 交点系列自定义节点空心圆圈 坐标标签XYChart.SeriesNumber,NumberintersectionSeriesnewXYChart.Series();intersectionSeries.setName(交点);for(double[]p:intersections){XYChart.DataNumber,NumberdatanewXYChart.Data(p[0],p[1]);// 自定义节点空心圆 标签StackPanenodenewStackPane();CirclecirclenewCircle(6);circle.setFill(null);// 空心circle.setStroke(Color.RED);circle.setStrokeWidth(2);LabellabelnewLabel(String.format((%.2f, %.2f),p[0],p[1]));label.setTextFill(Color.BLUE);label.setStyle(-fx-font-size: 12px; -fx-font-weight: bold;);label.setTranslateY(-20);// 标签上移避免遮挡node.getChildren().addAll(circle,label);data.setNode(node);intersectionSeries.getData().add(data);}// 添加所有系列到图表先添加曲线再添加交点保证交点在最上层chart.getData().addAll(series1,series2,intersectionSeries);// 可选设置交点系列默认样式透明以防默认样式干扰intersectionSeries.getNode().setStyle(-fx-stroke: transparent; -fx-background-color: transparent;);// 显示窗口ScenescenenewScene(chart,800,600);primaryStage.setScene(scene);primaryStage.show();}/** * 数值求解交点差值函数 h(x)f1(x)-f2(x) 的零点 */privateListdouble[]findIntersections(doublestep,doublestartX,doubleendX){Listdouble[]resultnewArrayList();intcount(int)((endX-startX)/step)1;double[]xValsnewdouble[count];double[]hValsnewdouble[count];for(inti0;icount;i){doublexstartXi*step;xVals[i]x;hVals[i]f1(x)-f2(x);}for(inti0;icount-1;i){doubleh1hVals[i];doubleh2hVals[i1];if(h1*h20){doublerootbisect(xVals[i],xVals[i1]);doubleyf1(root);result.add(newdouble[]{root,y});}// 处理恰好为零的点if(Math.abs(h1)1e-12){doubleyf1(xVals[i]);result.add(newdouble[]{xVals[i],y});}}if(Math.abs(hVals[count-1])1e-12){doubleyf1(xVals[count-1]);result.add(newdouble[]{xVals[count-1],y});}returnresult;}/** * 二分法求根精度 1e-10 */privatedoublebisect(doublea,doubleb){doubleeps1e-10;doublemid;intmaxIter100;for(inti0;imaxIter;i){mid(ab)/2;doublefaf1(a)-f2(a);doublefmf1(mid)-f2(mid);if(Math.abs(fm)eps)returnmid;if(fa*fm0)bmid;elseamid;}return(ab)/2;}publicstaticvoidmain(String[]args){launch(args);}}