如何在 Java 中调用 MATLAB 代码

时间:2022-12-14 16:57:09

文章目录

运行环境:

  • MATLAB R2022a

  • Java 8(1.8.0_311)

  • IntelliJ IDEA 2022.2.1 (Ultimate Edition)

  • Maven 3.8.3

  • Windows 10 教育版 64位

  使用混合编程通常都不是好主意,但是有时候会遇到极端的情况。Java 擅长网络编程,MATLAB 擅长数学高级计算与图形化。这种情况下,没办法使用一种编程语言快速完成这两项事情,因此不得不使用 Java、MATLAB 混合编程。这里提供的办法是,将一个 MATLAB 函数文件转化为 Java 的 JAR 包,然后在 Java 中运行这个 JAR 包。

  在 Java 中调用 MATLAB 代码,方法其实有很多,这里给出的是一种朴素的方法:先将 MATLAB 代码转化为 Java 的 JAR 包,然后在 Java 程序中就可以直接调用这个 JAR 包了。

  编写这个教程时,笔者已经帮读者踩了很多坑。仔细阅读本教程可以减少很多麻烦。

  1. 这里编写了一个简单的 MATLAB 函数 matlabPlot、matlabPolarplot。这两个函数做的事情很简单,只是调用原生函数用于绘图而已。只不过一个是以直角坐标系作图,另一个是极坐标系。

    • matlabPlot.m
    function matlabPlot(x,y)
    plot(x,y);
    axis equal;
    
    • matlabPolarplot.m
    function matlabPolarplot(x,y)
    polarplot(x,y);
    
  2. 下面需要将上面那两个 MATLAB 函数文件打包为 JAR,类名为 MatlabUtil。关于这方面的内容,可见笔者的另一篇博客:

    如何将 MATLAB 源代码导出成 Java 的 JAR 包:
    https://blog.csdn.net/wangpaiblog/article/details/127957144

  3. 现在假设读者已经完成了打 JAR 包的步骤。但是,光有此 JAR 包还不能在我们自己的 Java 程序中运行。因为显然,此 JAR 包本质上只会含我们上面写的那么一点儿代码,这肯定是无法运行的。运行肯定还需要 MATLAB 自身对外提供的 SDK,也就是编程时经常所说的运行环境。不过幸运的是,这个 Java 版本的 SDK 在 MATLAB 安装的时候就已经提供了。对于笔者的 MATLAB R2022a,它在如下目录中。读者需要根据自己 MATLAB 的安装情况找到那个名为 javabuilder.jar 的 JAR 包。

    C:\Program Files\MATLAB\R2022a\toolbox\javabuilder\jar\javabuilder.jar
    
  4. 因此,这里只需要 MatlabUtil.jarjavabuilder.jar 即可在我们自己的 Java 程序中运行。


    【踩坑提醒】

      这两个 JAR 包不能在 Java 8 以上的版本运行,否则会发生如下报错:

       Exception in thread "AWT-EventQueue-0": java.lang.IllegalAccessError: superclass access check failed: class com.mathworks.hg.peer.types.HGMotifCheckMenuUI (in unnamed module @0x706bf110) cannot access class com.sun.java.swing.plaf.motif.MotifMenuUI (in module java.desktop) because module java.desktop does not export com.sun.java.swing.plaf.motif to unnamed module @0x706bf110
       	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
       	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
       	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
       	at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:524)
       	at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:427)
       	at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:421)
       	at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
       	at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:420)
       	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587)
       	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
       	at com.mathworks.hg.peer.MenuPeer.doCreateMenu(MenuPeer.java:142)
       	at com.mathworks.hg.peer.MenuPeer.access$000(MenuPeer.java:32)
       	at com.mathworks.hg.peer.MenuPeer$1.run(MenuPeer.java:131)
       	at com.mathworks.hg.util.HGPeerQueue$HGPeerRunnablesRunner.runit(HGPeerQueue.java:290)
       	at com.mathworks.hg.util.HGPeerQueue$HGPeerRunnablesRunner.runThese(HGPeerQueue.java:318)
       	at com.mathworks.hg.util.HGPeerQueue$HGPeerRunnablesRunner.run(HGPeerQueue.java:335)
       	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
       	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:771)
       	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
       	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:716)
       	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
       	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
       	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:741)
       	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
       	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
       	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
       	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
       	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
    

      Java 允许在同一个操作系统上安装多个版本的 Java。由于 Java 的 IDE 普遍支持在 IDE 中*选择 Java 版本,所以这可能导致问题。

      如果想确认自己的 Java 程序使用的是哪个版本的 Java,可以在程序使用以下代码之一。

    System.out.println("Java 版本号:" + System.getProperty("java.version"));
    System.out.println("Java 虚拟机规范版本号:" + System.getProperty("java.vm.specification.version"));
    System.out.println("Java 规范版本号:" + System.getProperty("java.specification.version"));
    System.out.println("Java 类路径:" + System.getProperty("java.class.path"));
    System.out.println("Java lib 路径:" + System.getProperty("java.library.path"));
    System.out.println("Java 执行路径:" + System.getProperty("java.ext.dirs"));
    

  5. 现在需要将上面那两个 JAR 包导入到我们的 Java 项目中去。根据读者使用的 Java 构建工具的不同,这个过程会有不同。

  6. 现在假设读者已经完成了导入 JAR 包的步骤。可以开始编写自己的 Java 程序了。为了使用到上面的直角坐标系作图函数,这里编写了一个绘制 双曲螺线 的 Java 代码,但是本文不打算详细介绍双曲螺线。

    • 双曲螺线参数方程:(x、y 分别为横、纵坐标)

      如何在 Java 中调用 MATLAB 代码

    • 双曲螺线 Java 代码:

      /**
       * 双曲螺线
       *
       * @since 2022-10-16
       */
      public static void hyperbolicSpiral() throws MWException {
          final long startTime = System.currentTimeMillis();
          double start = PI;
          double end = 1000 * PI;
          double interval = PI / 100;
          int pointNum = (int) ((end - start) / interval);
          int[] dimensions = {1, pointNum};
          MWNumericArray x = MWNumericArray.newInstance(dimensions, MWClassID.DOUBLE, MWComplexity.REAL);
          MWNumericArray y = MWNumericArray.newInstance(dimensions, MWClassID.DOUBLE, MWComplexity.REAL);
          final double c = 100;
          for (int i = 1; i <= pointNum; ++i) {
              double ti = i * interval + start;
              double xi = c * cos(ti) / ti;
              x.set(i, xi);
              double yi = c * sin(ti) / ti;
              y.set(i, yi);
          }
          MatlabUtil matlabUtil = null;
          try {
              matlabUtil = new MatlabUtil();
              matlabUtil.matlabPlot(x, y); // 在 MATLAB 绘制完图像界面之后,此方法会返回
              System.out.printf("双曲螺线绘制用时:%fs%n", (System.currentTimeMillis() - startTime) / 1000.0);
              matlabUtil.waitForFigures(); // 在用户关闭 MATLAB 图像界面之前,此方法会阻塞当前线程
          } finally {
              // 释放 MATLAB 图像界面资源。一旦释放之后,当前 MATLAB 图像界面会*关闭
              MWArray.disposeArray(x);
              MWArray.disposeArray(y);
              if (matlabUtil != null) {
                  matlabUtil.dispose();
              }
          }
      }
      
    • 程序运行结果:

      如何在 Java 中调用 MATLAB 代码

  7. 现在来试一试极坐标绘图。这里选择用极坐标绘制 蝴蝶曲线

    • 蝴蝶曲线极坐标方程:(x、y 分别为极角、极径坐标)

      如何在 Java 中调用 MATLAB 代码

    • 蝴蝶曲线 Java 代码:

      /**
       * 蝴蝶曲线
       *
       * @since 2022-10-16
       */
      public static void butterflyCurve() throws MWException {
          final long startTime = System.currentTimeMillis();
          double start = 0;
          double end = 20 * PI;
          double interval = PI / 50;
          int pointNum = (int) ((end - start) / interval);
          int[] dimensions = {1, pointNum};
          MWNumericArray x = MWNumericArray.newInstance(dimensions, MWClassID.DOUBLE, MWComplexity.REAL);
          MWNumericArray y = MWNumericArray.newInstance(dimensions, MWClassID.DOUBLE, MWComplexity.REAL);
          for (int i = 1; i <= pointNum; ++i) {
              double xi = i * interval + start;
              x.set(i, xi);
              double yi = exp(cos(xi - PI / 2)) - 2 * cos(4 * (xi - PI / 2)) + pow(sin((xi - PI / 2) / 12), 5);
              y.set(i, yi);
          }
          MatlabUtil matlabUtil = null;
          try {
              matlabUtil = new MatlabUtil();
              matlabUtil.matlabPolarplot(x, y); // 在 MATLAB 绘制完图像界面之后,此方法会返回
              System.out.printf("蝴蝶曲线绘制用时:%fs%n", (System.currentTimeMillis() - startTime) / 1000.0);
              matlabUtil.waitForFigures(); // 在用户关闭 MATLAB 图像界面之前,此方法会阻塞当前线程
          } finally {
              // 释放 MATLAB 图像界面资源。一旦释放之后,当前 MATLAB 图像界面会*关闭
              MWArray.disposeArray(x);
              MWArray.disposeArray(y);
              if (matlabUtil != null) {
                  matlabUtil.dispose();
              }
          }
      }
      
    • 程序运行结果:

      如何在 Java 中调用 MATLAB 代码

测评

  程序编写完之后,评估一下性能是一件顺理成章的事情。

  为此,笔者之前留了一手,在前面的 Java 程序插入了程序运行时间统计。运行结果表明,在 Java 中调用 MATLAB 绘图,两个程序的运行时间都大致在 13s 左右(根据不同机器的运行环境,此结果仅供参考)。

  现在来试试其在 MATLAB 中的运行情况。

  为此,这里同样对应编写了两个 MATLAB 脚本。这两个脚本的内容非常简单,这里不作解释。

  • hyperbolicSpiral.m

    clear
    clc
    
    tic;
    
    t=pi:pi/100:1000*pi;
    c=100;
    x=c*cos(t)./t;
    y=c*sin(t)./t;
    plot(x,y);
    axis equal;
    
    toc;
    
  • butterflyCurve.m

    clear
    clc
    
    tic;
    
    x=0:pi/50:20*pi;
    y=exp(cos(x-pi/2))-2*cos(4*(x-pi/2))+sin((x-pi/2)/12).^5;
    polarplot(x,y);
    
    toc;
    

  这两个脚本的运行结果表明,hyperbolicSpiral、butterflyCurve 的运行时间均大致为 0.07 秒。而如果之前没有关闭上一次的 MATLAB 图像界面,这个时间可以分别降到 0.02、0.01 秒(根据不同机器的运行环境,此结果仅供参考)。

  可以看出,就算是算作最坏情况——第一次运行(没有借用上一次的 GUI 资源),使用 MATLAB 原生方式运行的时间比使用 Java 大概快了 200 倍!

  我们知道,MATLAB 的核心是用 C 语言编写的,这是不是说 Java 比 C 语言慢 200 倍呢?这种说法可不太精确。作为编程人员,我们知道,Java 是比纯 C 语言要慢。不过,作为一段程序,它的运行时间由最耗时的那段决定。对于这种需要 GUI 界面显示图像的程序,如果计算过程很简单的话,运行时间都主要耗在 GUI 上。在 Java 8 中,最先进的 GUI 就是 Swing 了。MATLAB 使用 Java 完成的图像显示就是使用 Swing 完成的。

  GUI 一直 Java 的软肋。作为 Java 的 GUI 技术 Swing、JavaFX 的长期使用人员,笔者可以很遗憾地告诉你,Java 的 GUI 应用无论是在启动时间、内存占用上,都比 C 家族编写的 GUI 应用逊色很多。因此,上面这之所以会出现这种离谱的现象,是因为它们各自在 UI 上耗费了大量的时间,而后台计算部分又过于简单。所以,如果编写的程序涉及计算的部分足够复杂,使用 Java 运行的时间也不会出现大幅度的增长。只能说,如果选择 Java 调用 MATLAB 绘图,不管程序有多简单,至少都要耗费 13 秒的时间。

  那么,读者就可能想问了,使用 Java 不仅运行速度慢,编写的代码还“又臭又长”,直接使用 MATLAB 不就可以了吗?这又回到文中开始的问题。混合编程一直都是不应提倡,能避免就避免。但笔者编写的很多程序,它们同时需要使用 Python 进行网上爬虫、使用 Java 连接数据库和服务器、使用 MATLAB 进行绘图展示。这无法通过纯 MATLAB 代码来完成其它操作。就算 MATLAB 提供了支持,使用起来也势必有很多短板。再加上笔者在此之前已经掌握了很多门编程语言,因此再学习新的语言也不是太有难度。

完整源代码

  本文上面演示的完整代码已上传至 GitHub 中,可免费下载。同时笔者也将不定期免费进行升级维护,更新的内容也会即时上传。GitHub 地址:https://github.com/wangpaiblog/20221212_java-calls-matlab