使用 JavaFX + Reactor 绘制函数图像并数值求解交点 —— 原理与实现

📅 2026/8/4 9:01:08 👁️ 阅读次数 📝 编程学习
使用 JavaFX + Reactor 绘制函数图像并数值求解交点 —— 原理与实现

使用 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中,我们直接将计算出的数据点加入seriesgetData().add(),该方法会在 JavaFX 应用程序线程中更新,但blockLast()会阻塞 UI 线程直到所有点添加完毕,这个过程只持续几十毫秒,用户无感知。


四、数值求解交点

4.1 原理

两个连续函数的交点满足 ( f_1(x) = f_2(x) ),等价于差值函数 ( h(x) = f_1(x) - f_2(x) ) 的零点。若 ( h(x) ) 在某区间两端异号,则根据介值定理,该区间内必存在至少一个根。

4.2 实现步骤

  1. 离散采样:在定义域内以步长step采样,计算每个点的 ( h(x) )。
  2. 符号检测:遍历相邻点,若h1 * h2 < 0,则说明该区间内存在根。
  3. 二分法精化:对每个含根区间,使用二分法(Bisection Method)迭代求根,直到区间宽度小于给定精度(1e-10)。
  4. 处理端点:若某点 ( 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:虽未直接用于此程序,但理解上下文传递有助于在更复杂的响应式流中传递调试信息。
  • SynchronousSinkFlux.generate曾用于创建无限流,而本例使用Flux.range生成有限点集,两者都是 Reactor 生成数据的常用方式。
  • Fuseable:内部优化,对于大量数据点可提升性能,但本例数据量不大,未显式使用。
  • 数值求交:延续了之前对数函数导数、三角数流等数值计算的思想,体现了响应式流在科学计算中的实用性。

八、运行与效果

运行程序后,窗口显示两条平滑曲线,交点处出现红色空心圆圈,上方附有蓝色坐标标签,清晰无遮挡。控制台无额外输出,全部信息可视化。


九、总结

本文通过一个完整的 Java 程序,展示了如何结合ReactorJavaFX

  • 响应式生成大量离散点以绘制平滑曲线。
  • 利用数值差分法(符号检测 + 二分法)求解函数交点。
  • 自定义图表节点,实现空心圆圈与标签标注。

该方案不依赖解析公式,可推广至任意连续函数,具有广泛的应用价值。掌握这些技术,将为后续的数值分析与可视化开发奠定坚实基础。

完整代码

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);}}