使用 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 代码解析

java 复制代码
double step = 0.01;
double startX = -5.0;
double endX = 5.0;
int count = (int) ((endX - startX) / step) + 1;

Flux.range(0, count)
    .map(i -> startX + i * step)
    .doOnNext(x -> {
        series1.getData().add(new XYChart.Data<>(x, f1(x)));
        series2.getData().add(new XYChart.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 坐标轴设置

java 复制代码
NumberAxis xAxis = new NumberAxis();
xAxis.setLowerBound(-5);
xAxis.setUpperBound(5);
xAxis.setAutoRanging(false);

固定 x 轴范围,确保定义域完整显示。y 轴则自动适应数据范围。

3.2 创建图表与系列

java 复制代码
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
XYChart.Series<Number, Number> series1 = new XYChart.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) ) 恰好为零(或绝对值极小),直接视为交点。
java 复制代码
private List<double[]> findIntersections(double step, double startX, double endX) {
    List<double[]> result = new ArrayList<>();
    int count = (int) ((endX - startX) / step) + 1;
    double[] xVals = new double[count];
    double[] hVals = new double[count];

    for (int i = 0; i < count; i++) {
        double x = startX + i * step;
        xVals[i] = x;
        hVals[i] = f1(x) - f2(x);
    }

    for (int i = 0; i < count - 1; i++) {
        if (hVals[i] * hVals[i+1] < 0) {
            double root = bisect(xVals[i], xVals[i+1]);
            double y = f1(root);
            result.add(new double[]{root, y});
        }
        if (Math.abs(hVals[i]) < 1e-12) {
            double y = f1(xVals[i]);
            result.add(new double[]{xVals[i], y});
        }
    }
    // 检查最后一点
    if (Math.abs(hVals[count-1]) < 1e-12) {
        double y = f1(xVals[count-1]);
        result.add(new double[]{xVals[count-1], y});
    }
    return result;
}

4.3 二分法细节

java 复制代码
private double bisect(double a, double b) {
    double eps = 1e-10;
    double mid;
    int maxIter = 100;
    for (int i = 0; i < maxIter; i++) {
        mid = (a + b) / 2;
        double fa = f1(a) - f2(a);
        double fm = f1(mid) - f2(mid);
        if (Math.abs(fm) < eps) return mid;
        if (fa * fm < 0) b = mid;
        else a = mid;
    }
    return (a + b) / 2;
}

二分法保证收敛,且精度可控。对于本例,只需几次迭代即可达到 1e-10 精度。


五、交点标注:自定义节点

5.1 需求

  • 使用空心圆圈标记交点位置。
  • 在圆圈上方显示坐标标签,避免遮挡曲线。

5.2 实现

为每个交点数据创建一个 XYChart.Data,并通过 setNode 设置自定义的 StackPane

java 复制代码
XYChart.Data<Number, Number> data = new XYChart.Data<>(p[0], p[1]);
StackPane node = new StackPane();
Circle circle = new Circle(6);
circle.setFill(null);        // 空心
circle.setStroke(Color.RED);
circle.setStrokeWidth(2);
Label label = new Label(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

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

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

完整代码

java 复制代码
public class FunctionPlotter extends Application {

    // 定义函数:导数 f1(x) = 3x² - 1
    private double f1(double x) { return 3 * x * x - 1; }

    // 定义函数:直线 f2(x) = 2x - 1
    private double f2(double x) { return 2 * x - 1; }

    @Override
    public void start(Stage primaryStage) {
        // 坐标轴
        NumberAxis xAxis = new NumberAxis();
        xAxis.setLabel("x");
        xAxis.setLowerBound(-5);
        xAxis.setUpperBound(5);
        xAxis.setAutoRanging(false);

        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("y");
        yAxis.setAutoRanging(true);

        // 图表
        LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
        chart.setTitle("导数函数与直线交点");
        chart.setCreateSymbols(false); // 不显示数据点默认符号,避免干扰

        double step = 0.01;
        double startX = -5.0;
        double endX = 5.0;
        int count = (int) ((endX - startX) / step) + 1;

        // 系列1:f1(x)
        XYChart.Series<Number, Number> series1 = new XYChart.Series<>();
        series1.setName("f1(x) = 3x² - 1");

        // 系列2:f2(x)
        XYChart.Series<Number, Number> series2 = new XYChart.Series<>();
        series2.setName("f2(x) = 2x - 1");

        // 使用 Reactor 生成数据点
        Flux.range(0, count)
                .map(i -> startX + i * step)
                .doOnNext(x -> {
                    series1.getData().add(new XYChart.Data<>(x, f1(x)));
                    series2.getData().add(new XYChart.Data<>(x, f2(x)));
                })
                .blockLast();

        // ---- 数值求交点 ----
        List<double[]> intersections = findIntersections(step, startX, endX);

        // 交点系列(自定义节点:空心圆圈 + 坐标标签)
        XYChart.Series<Number, Number> intersectionSeries = new XYChart.Series<>();
        intersectionSeries.setName("交点");
        for (double[] p : intersections) {
            XYChart.Data<Number, Number> data = new XYChart.Data<>(p[0], p[1]);
            // 自定义节点:空心圆 + 标签
            StackPane node = new StackPane();
            Circle circle = new Circle(6);
            circle.setFill(null);                // 空心
            circle.setStroke(Color.RED);
            circle.setStrokeWidth(2);
            Label label = new Label(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;");

        // 显示窗口
        Scene scene = new Scene(chart, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * 数值求解交点(差值函数 h(x)=f1(x)-f2(x) 的零点)
     */
    private List<double[]> findIntersections(double step, double startX, double endX) {
        List<double[]> result = new ArrayList<>();
        int count = (int) ((endX - startX) / step) + 1;
        double[] xVals = new double[count];
        double[] hVals = new double[count];

        for (int i = 0; i < count; i++) {
            double x = startX + i * step;
            xVals[i] = x;
            hVals[i] = f1(x) - f2(x);
        }

        for (int i = 0; i < count - 1; i++) {
            double h1 = hVals[i];
            double h2 = hVals[i + 1];
            if (h1 * h2 < 0) {
                double root = bisect(xVals[i], xVals[i + 1]);
                double y = f1(root);
                result.add(new double[]{root, y});
            }
            // 处理恰好为零的点
            if (Math.abs(h1) < 1e-12) {
                double y = f1(xVals[i]);
                result.add(new double[]{xVals[i], y});
            }
        }
        if (Math.abs(hVals[count - 1]) < 1e-12) {
            double y = f1(xVals[count - 1]);
            result.add(new double[]{xVals[count - 1], y});
        }
        return result;
    }

    /**
     * 二分法求根(精度 1e-10)
     */
    private double bisect(double a, double b) {
        double eps = 1e-10;
        double mid;
        int maxIter = 100;
        for (int i = 0; i < maxIter; i++) {
            mid = (a + b) / 2;
            double fa = f1(a) - f2(a);
            double fm = f1(mid) - f2(mid);
            if (Math.abs(fm) < eps) return mid;
            if (fa * fm < 0) b = mid;
            else a = mid;
        }
        return (a + b) / 2;
    }

    public static void main(String[] args) {
        launch(args);
    }
}
相关推荐
摇滚侠5 小时前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 监控指标 Metrics 阅读笔记
java·spring boot·笔记
玖石书6 小时前
ASP.NET Core 迁移至 Spring系列:编译生态篇
后端·spring·asp.net
弹简特6 小时前
【Java项目-企悦抽】11-用户与认证模块-实现用户登录03(结束登录功能)-完成测试+对接前端+登录拦截器
java·开发语言
大模型丫丫6 小时前
Loop Engineering:从强化学习视角看 Agent 循环的本质
java·人工智能·学习
数电发票API6 小时前
税局接口,需要的来~~
java
骇客野人6 小时前
IntelliJ IDEA 使用技巧
java·ide·intellij-idea
Jul1en_7 小时前
【Java 脚手架】封装通用工具类-1
java·spring boot·分布式·spring·spring cloud
我命由我123457 小时前
Java 开发 - List subList 方法
java·windows·操作系统·list·intellij-idea·idea·intellij idea
关于不上作者榜就原神启动那件事7 小时前
从 MDC 到 Agent:我手搓的文档路由协议,在 Spring AI Alibaba 里找到了正式实现
java·人工智能·spring·ai·agent
亦暖筑序8 小时前
重新认识 AgentScope-Java 2.0:ReActAgent 负责推理,HarnessAgent 负责运行
java·后端·agent