使用 JavaFX + Reactor 绘制函数图像并数值求解交点 —— 原理与实现
在数值计算与可视化中,我们常常需要绘制函数曲线并标注其交点。本文将以一个完整的 Java 程序为例,深入剖析如何利用Reactor生成数据点、JavaFX绘制曲线,并通过纯数值方法(符号检测 + 二分法)求解交点,最终在图上以空心圆圈和坐标标签的形式清晰显示。
代码全程不依赖任何数学求导公式,完全基于离散点采样与迭代逼近,具有通用性强、易于扩展的特点。
一、程序概览
我们定义两个函数:
- ( f1(x) = 3x^2 - 1 )(二次函数)
- ( f2(x) = 2x - 1 )(一次函数)
目标是绘制它们在区间 ([-5, 5]) 上的图像,并求出所有交点坐标,在图上用空心圆标注。
二、数据生成:Reactor 的Flux流
2.1 为何使用 Reactor?
Reactor 提供了一套响应式编程 API,其Flux可以方便地生成大量离散数据点,并以声明式的方式进行处理。与传统 for 循环相比,Flux更易于组合、并行处理和链式操作。
2.2 代码解析
doublestep=0.01;doublestartX=-5.0;doubleendX=5.0;intcount=(int)((endX-startX)/step)+1;Flux.range(0,count).map(i->startX+i*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 的LineChart
3.1 坐标轴设置
NumberAxisxAxis=newNumberAxis();xAxis.setLowerBound(-5);xAxis.setUpperBound(5);xAxis.setAutoRanging(false);固定 x 轴范围,确保定义域完整显示。y 轴则自动适应数据范围。
3.2 创建图表与系列
LineChart<Number,Number>chart=newLineChart<>(xAxis,yAxis);XYChart.Series<Number,Number>series1=newXYChart.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) ) 恰好为零(或绝对值极小),直接视为交点。
privateList<double[]>findIntersections(doublestep,doublestartX,doubleendX){List<double[]>result=newArrayList<>();intcount=(int)((endX-startX)/step)+1;double[]xVals=newdouble[count];double[]hVals=newdouble[count];for(inti=0;i<count;i++){doublex=startX+i*step;xVals[i]=x;hVals[i]=f1(x)-f2(x);}for(inti=0;i<count-1;i++){if(hVals[i]*hVals[i+1]<0){doubleroot=bisect(xVals[i],xVals[i+1]);doubley=f1(root);result.add(newdouble[]{root,y});}if(Math.abs(hVals[i])<1e-12){doubley=f1(xVals[i]);result.add(newdouble[]{xVals[i],y});}}// 检查最后一点if(Math.abs(hVals[count-1])<1e-12){doubley=f1(xVals[count-1]);result.add(newdouble[]{xVals[count-1],y});}returnresult;}4.3 二分法细节
privatedoublebisect(doublea,doubleb){doubleeps=1e-10;doublemid;intmaxIter=100;for(inti=0;i<maxIter;i++){mid=(a+b)/2;doublefa=f1(a)-f2(a);doublefm=f1(mid)-f2(mid);if(Math.abs(fm)<eps)returnmid;if(fa*fm<0)b=mid;elsea=mid;}return(a+b)/2;}二分法保证收敛,且精度可控。对于本例,只需几次迭代即可达到1e-10精度。
五、交点标注:自定义节点
5.1 需求
- 使用空心圆圈标记交点位置。
- 在圆圈上方显示坐标标签,避免遮挡曲线。
5.2 实现
为每个交点数据创建一个XYChart.Data,并通过setNode设置自定义的StackPane。
XYChart.Data<Number,Number>data=newXYChart.Data<>(p[0],p[1]);StackPanenode=newStackPane();Circlecircle=newCircle(6);circle.setFill(null);// 空心circle.setStroke(Color.RED);circle.setStrokeWidth(2);Labellabel=newLabel(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 x=0 \text{ 或 } x=\frac{2}{3}
]
对应 y 值为 -1 和 1/3。
数值解在1e-10精度下完全吻合,验证了算法的正确性。这证明了在步长足够小、二分法精度足够高的情况下,数值方法能可靠地逼近解析解。
七、与之前学习内容的联系
Context/ContextView:虽未直接用于此程序,但理解上下文传递有助于在更复杂的响应式流中传递调试信息。SynchronousSink:Flux.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){// 坐标轴NumberAxisxAxis=newNumberAxis();xAxis.setLabel("x");xAxis.setLowerBound(-5);xAxis.setUpperBound(5);xAxis.setAutoRanging(false);NumberAxisyAxis=newNumberAxis();yAxis.setLabel("y");yAxis.setAutoRanging(true);// 图表LineChart<Number,Number>chart=newLineChart<>(xAxis,yAxis);chart.setTitle("导数函数与直线交点");chart.setCreateSymbols(false);// 不显示数据点默认符号,避免干扰doublestep=0.01;doublestartX=-5.0;doubleendX=5.0;intcount=(int)((endX-startX)/step)+1;// 系列1:f1(x)XYChart.Series<Number,Number>series1=newXYChart.Series<>();series1.setName("f1(x) = 3x² - 1");// 系列2:f2(x)XYChart.Series<Number,Number>series2=newXYChart.Series<>();series2.setName("f2(x) = 2x - 1");// 使用 Reactor 生成数据点Flux.range(0,count).map(i->startX+i*step).doOnNext(x->{series1.getData().add(newXYChart.Data<>(x,f1(x)));series2.getData().add(newXYChart.Data<>(x,f2(x)));}).blockLast();// ---- 数值求交点 ----List<double[]>intersections=findIntersections(step,startX,endX);// 交点系列(自定义节点:空心圆圈 + 坐标标签)XYChart.Series<Number,Number>intersectionSeries=newXYChart.Series<>();intersectionSeries.setName("交点");for(double[]p:intersections){XYChart.Data<Number,Number>data=newXYChart.Data<>(p[0],p[1]);// 自定义节点:空心圆 + 标签StackPanenode=newStackPane();Circlecircle=newCircle(6);circle.setFill(null);// 空心circle.setStroke(Color.RED);circle.setStrokeWidth(2);Labellabel=newLabel(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;");// 显示窗口Scenescene=newScene(chart,800,600);primaryStage.setScene(scene);primaryStage.show();}/** * 数值求解交点(差值函数 h(x)=f1(x)-f2(x) 的零点) */privateList<double[]>findIntersections(doublestep,doublestartX,doubleendX){List<double[]>result=newArrayList<>();intcount=(int)((endX-startX)/step)+1;double[]xVals=newdouble[count];double[]hVals=newdouble[count];for(inti=0;i<count;i++){doublex=startX+i*step;xVals[i]=x;hVals[i]=f1(x)-f2(x);}for(inti=0;i<count-1;i++){doubleh1=hVals[i];doubleh2=hVals[i+1];if(h1*h2<0){doubleroot=bisect(xVals[i],xVals[i+1]);doubley=f1(root);result.add(newdouble[]{root,y});}// 处理恰好为零的点if(Math.abs(h1)<1e-12){doubley=f1(xVals[i]);result.add(newdouble[]{xVals[i],y});}}if(Math.abs(hVals[count-1])<1e-12){doubley=f1(xVals[count-1]);result.add(newdouble[]{xVals[count-1],y});}returnresult;}/** * 二分法求根(精度 1e-10) */privatedoublebisect(doublea,doubleb){doubleeps=1e-10;doublemid;intmaxIter=100;for(inti=0;i<maxIter;i++){mid=(a+b)/2;doublefa=f1(a)-f2(a);doublefm=f1(mid)-f2(mid);if(Math.abs(fm)<eps)returnmid;if(fa*fm<0)b=mid;elsea=mid;}return(a+b)/2;}publicstaticvoidmain(String[]args){launch(args);}}