运行环境:
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 包了。
编写这个教程时,笔者已经帮读者踩了很多坑。仔细阅读本教程可以减少很多麻烦。
-
这里编写了一个简单的 MATLAB 函数 matlabPlot、matlabPolarplot。这两个函数做的事情很简单,只是调用原生函数用于绘图而已。只不过一个是以直角坐标系作图,另一个是极坐标系。
matlabPlot.m
function matlabPlot(x,y) plot(x,y); axis equal;
matlabPolarplot.m
function matlabPolarplot(x,y) polarplot(x,y);
-
下面需要将上面那两个 MATLAB 函数文件打包为 JAR,类名为 MatlabUtil。关于这方面的内容,可见笔者的另一篇博客:
如何将 MATLAB 源代码导出成 Java 的 JAR 包:
https://blog.csdn.net/wangpaiblog/article/details/127957144 -
现在假设读者已经完成了打 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
-
因此,这里只需要
MatlabUtil.jar
和javabuilder.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"));
-
现在需要将上面那两个 JAR 包导入到我们的 Java 项目中去。根据读者使用的 Java 构建工具的不同,这个过程会有不同。
-
对于不使用构建工具的情况,可见笔者的另一篇博客:
Java 库文件的添加教程:
https://blog.csdn.net/wangpaiblog/article/details/111772193 -
如果使用构建工具 Maven,可将其作为本地依赖。更多的信息,可见笔者的另一篇博客:
如何在 Maven 中通过本地路径使用 JAR 包依赖:
https://blog.csdn.net/wangpaiblog/article/details/127840334
-
-
现在假设读者已经完成了导入 JAR 包的步骤。可以开始编写自己的 Java 程序了。为了使用到上面的直角坐标系作图函数,这里编写了一个绘制
双曲螺线
的 Java 代码,但是本文不打算详细介绍双曲螺线。-
双曲螺线参数方程:(x、y 分别为横、纵坐标)
-
双曲螺线 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(); } } }
-
程序运行结果:
-
-
现在来试一试极坐标绘图。这里选择用极坐标绘制
蝴蝶曲线
。-
蝴蝶曲线极坐标方程:(x、y 分别为极角、极径坐标)
-
蝴蝶曲线 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 程序插入了程序运行时间统计。运行结果表明,在 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