小不的笔记

时间之外的往事

git pull error git config pull.rebase

正常用了好久的git项目突然pull命令不好使了,提示如下。这是是因为git在2.27版本后新增了一个显示的配置式,选择git的默认提交合并。

Pulling without specifying how to reconcile divergent branches is discouraged. You can squelch this message by running one of the following commands sometime before your next pull:

git config pull.rebase false     # merge (the default strategy)
git config pull.rebase true      # rebase
git config pull.ff only               # fast-forward only

You can replace “git config” with “git config –global” to set a default preference for all repositories. You can also pass –rebase, –no-rebase, or –ff-only on the command line to override the configured default per invocation.

作为开发者就选第一个默认策略就好了。

1
git config pull.rebase false

不想每个项目单独配置就加上--global标记作为全局配置

1
git config --global pull.rebase false

默认策略下git pull 其实相当于 git fetch 然后 git merge FETCH_HEAD,会生成一个merge的提交。提交记录整洁没有稀奇古怪的东西,就可以使用rebase

如果希望使用rebase替代merge,就配置

1
git config pull.rebase true

rebase 还是 merge 要看项目本身的要求。

如果主要是做更新,很少做任何提交,可以配置如下,该配置下如果不能快速合并就会报错。

1
git config pull.ff only

这个模式下不是说就不能提交代码了,可以依旧通过手动的fetch然后mergerebase来更新分支。相当于自动挡换手动挡了。

jdk8-method-reference

方法引用

简介

方法引用是一种在 Java 中使用现有方法来创建简洁的 lambda 表达式的方式。它可以让你直接指向现有方法,而无需重新定义方法体。方法引用可以使代码更加简洁、易读,并减少冗余。

种类

方法引用有四种类型:

  1. 静态方法引用: 引用静态方法,例如 ClassName::staticMethodName
  2. 特定对象的实例方法引用: 引用特定对象的实例方法,例如 containingObject::instanceMethodName
  3. 任意对象的实例方法引用: 引用特定类型的任意对象的实例方法,例如 ContainingType::methodName
  4. 构造函数引用: 引用构造函数,例如 ClassName::new

示例

  1. 静态方法引用:

    Java

    1
    Arrays.sort(rosterAsArray, Person::compareByAge);

    等价于:

    Java

    1
    Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b));

    Use code with caution. Learn more

    content_copy

  2. 特定对象的实例方法引用:

    Java

    1
    2
    ComparisonProvider myComparisonProvider = new ComparisonProvider();
    Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

    Use code with caution. Learn more

    content_copy

  3. 任意对象的实例方法引用:

    Java

    1
    2
    String[] stringArray = { "Barbara", "James", ... };
    Arrays.sort(stringArray, String::compareToIgnoreCase);

    Use code with caution. Learn more

    content_copy

  4. 构造函数引用:

    Java

    1
    Set<Person> rosterSet = transferElements(roster, HashSet::new);

    Use code with caution. Learn more

    content_copy

总结

方法引用是一种简洁、高效的代码编写方式,可以有效地减少代码量,提高代码的可读性。熟练掌握方法引用可以使你编写出更加简洁、优雅的 Java 代码。

Java8 default method

Java8 引入”默认方法”的新特性,它允许开发者不破坏现有的接口实现的前提下给接口添加新的方法。它提供了灵活性,允许接口定义实现并在具体类未提供该方法的实现时使用默认实现。这个特性部分实现了多重继承的功能,在接口的演化和代码复用方面都非常实用。

Java8加入了很多方法都是通过默认方法引入的,比如forEach、stream、parallelStream。

默认方法的语法

在接口里实现一个用default修饰的方法。

1
2
3
4
5
interface IFoo {  
default void hello() {
System.out.println("Hello IFoo!");
}
}

多重继承

当我们继承多个接口,而这些接口有相同的方法签名时,该接口需要显示的声明该方法。如果声明就会报错。

1
2
3
4
5
6
7
8
interface IBar {  
default void hello() {
System.out.println("Hello IBar!");
}
}
interface IFoobar extends IFoo, IBar {

}

这样会报错:

IFoobar inherits unrelated defaults for hello() from types IFoo and IBar。

可以通过实现这个方法来修正这个错误。

1
2
3
interface IFoobar extends IFoo, IBar {  
default void hello() {}
}

如果想复用某个接口的实现,可以通过接口名.super.方法名来实现。

1
2
3
4
5
interface IFoobar extends IFoo, IBar {  
default void hello() {
IFoo.super.hello();
}
}

static 方法

Java8的接口也可以定义static方法。

1
2
3
4
5
6
7
8
9
interface IBar {  
default void hello() {
System.out.println("Hello IBar!");
}

static void staticHello(){
System.out.println("Hello static IBar!");
}
}

可以通过IBar.staticHello()调用这个方法。要注意的是static方法不能继承,不能通过IFoobar.staticHello()的方式调用。否则会有如下报错

Static method may be invoked on containing interface class only

dorado unkown package

正常部署的dorado项目在启动一段时间之后访问会报unkown package [widget]。因为dorado在启动时会把资源文件解压到临时目录里,可能是资源文件被清理掉导致的错误。

可以通过属性core.tempDir指定临时文件目录。

core.tempDir=./.dorado

或者关闭临时文件加速core.supportsTempFile=false

参考源码

  • com.bstek.dorado.web.loader.DoradoLoader.preload(boolean)
  • com.bstek.dorado.view.resolver.BootPackagesResolver.PackageResource.PackageResource(PackagesConfig, String, int, Locale)

Maven 跳过测试 skip test

跳过测试并不是一个好的做法,但是很多时候不得不这么做:D

通过命令行

1
2
3
4
5
6
7
8
9
# 编译测试类,跳过测试类的执行
mvn clean package -DskipTests
# 跳过测试类的编译和执行
mvn clean package -Dmaven.test.skip=true

# 跳过测试类的编译和执行,PowerShell 专属写法。
mvn clean package --% -Dmaven.test.skip=true
mvn clean package `-Dmaven.test.skip=true
mvn clean package '-Dmaven.test.skip=true'

PowerShell有自己特殊的参数处理方式,会截断-Dmaven.test.skip=true为两个参数,导致命令执行失败,需要额外的转义,也可以用`, 单引号。

默认跳过测试

命令行参数的方式需要每次执行命令的时候添加额外的参数,如果希望默认情况下就跳过测试阶段,可以通过配置skipTestsmaven.test.skip属性。

1
2
3
4
5
<properties>
<skipTests>true</skipTests>
<!-- 或 -->
<maven.test.skip>true</maven.test.skip>
</properties>

-Dxxx 和 <properties> 都是Maven配置属性的一种方式, 前者的优先级高于后者。

参考链接

Maven Surefire Plugin – Skipping Tests
IntegrationTestMojo.java

Java/Shell程序封装为MacOS程序 Application

macOS下有些带GUI的程序并没有按照macOS的规范去打包成一个MacOS的Application,导致启动它的体验不太好,虽然不影响使用。 这里以Jadx-- 一个Java反编译工具, 为例演示如何把一个Java程序包装为MacOS程序 Application。

通过brew安装过jadx之后,可以通过执行jadx-gui命令来启动他的图形界面。

做个图标

做为一个应用程序,最早呈现给用户的就是图标。macOS 使用 icns 格式的图标资源。图片格式及大小没具体要求,长宽比例必须是1:1的。 * 可以通过cloudconvert.com在线转换 https://cloudconvert.com/png-to-icns * 可以通过makeicns线下转换

1
2
brew install makeicns
makeicns -in myfile.png -out outfile.icns

改进Java显示效果

Java的GUI启动之后,在dock里还是显示JRE的默认图标,如果需要额外定制图标就需要通过其他参数指定。 Java为macOS提供两个专门的参数来定制在macOS的表现(在macOS上可以通过java -X 命令查看参数描述)。

1
2
3
4
-Xdock:name=<application name>
override default application name displayed in dock
-Xdock:icon=<path to icon file>
override default icon displayed in dock

-Xdock:name 用来指定在菜单栏里显示的名字。在macOS早期的版本是可以定制在dock中显示的名字。现在不行了。 -Xdock:icon= 用来指定在dock中显示的图标。 jadx-gui 支持通过JADX_GUI_OPTS变量给他传参数,其他Java程序按自己实际情况调整。

Automator

Automator 是macOS自带的自动化工具。 1. 启动 Automator 并创建一个 Application 类型的文件。 2. 添加一个Run Shell Script的action。Pass input改为as arguments并在下方的文本框内填入如下命令

1
JADX_GUI_OPTS="-Xdock:icon=/Applications/Jadx.app/Contents/Resources/ApplicationStub.icns -Xdock:name=Jadx" /usr/local/bin/jadx-gui "$@"
  1. 点击文件-保存。名字为Jadx.app, 位置选/Application,文件类型为Application
  2. 把定制的图标重命名为ApplicationStub.icns,然后覆盖位于/Application/Jadx.app/Contents/Resources/ApplicationStub.icns的Automator默认图标。 > 在 Xxx.app 上点右键,选择 显示包内容(Show Package Contents),就可以以文件夹的方式打开Xxx.app了。

universalJavaApplicationStub

使用 Automator 包装的程序制作简单,唯独在dock中显示的名字不能修改,略有遗憾。参考了JD-GUI的实现,我发现了universalJavaApplicationStub

miniBundleApp

最小可运行的macOS程序结构大概是这个样子。 * Info.plist 存储程序相关的属性配置信息。 * MacOS 目录存放可执行程序 * Resources 存放资源文件, 示例中放了图标资源。而像JD-GUI就把jar包放在Resources/Java目录下。

1
2
3
4
5
6
7
Demo.app
└── Contents
├── Info.plist
├── MacOS
│   └── universalJavaApplicationStub
└── Resources
└── ApplicationStub.icns
  1. 创建目录结构。
1
2
3
mkdir -p /Applications/Jadx-gui.app/Contents/{MacOS,Resources}
curl https://raw.githubusercontent.com/tofi86/universalJavaApplicationStub/master/src/universalJavaApplicationStub -o /Applications/Jadx-gui.app/Contents/MacOS/universalJavaApplicationStub
chmod +x /Applications/Jadx-gui.app/Contents/MacOS/universalJavaApplicationStub
  1. 配置Info.plist Info.plist 位于 Demo.app/Contents/Info.plist。 主要关系几个属性:CFBundleName、CFBundleIdentifier、MainClass、ClassPath。

* CFBundleName 程序名称 * CFBundleIdentifier 程序唯一标识 * MainClass Java的启动class * ClassPath Java的classpath路径。要注意的是如果要配置目录时,lib/* 需要对*做转义lib/\\*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key> <string>universalJavaApplicationStub</string>
<key>CFBundleName</key> <string>Jadx</string>
<key>CFBundleGetInfoString</key> <string>Jadx for macOS</string>
<key>CFBundleIconFile</key> <string>ApplicationStub.icns</string>
<key>CFBundleIdentifier</key> <string>org.xobo.Jadx</string>
<key>CFBundleShortVersionString</key> <string>1.0.0</string>
<key>NSHumanReadableCopyright</key> <string>Copyright 2022 xobo</string>


<key>JavaX</key>
<dict>
<key>MainClass</key> <string>jadx.gui.JadxGUI</string>
<key>JVMVersion</key> <string>1.8+</string>
<key>ClassPath</key> <string>/usr/local/Cellar/jadx/1.4.4/libexec/lib/\\*</string>
<key>WorkingDirectory</key> <string>$APP_ROOT</string>
<key>Properties</key>
<dict>
<key>apple.laf.useScreenMenuBar</key>
<string>true</string>
</dict>
</dict>

<key>CFBundlePackageType</key> <string>APPL</string>
<key>CSResourcesFileMapped</key> <true/>
<key>LSRequiresCarbon</key> <true/>
<key>NSPrincipalClass</key> <string>NSApplication</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>NSHighResolutionCapable</key> <true/>

<key>NSAppleEventsUsageDescription</key>
<string>There was an error while launching the application. Please click OK to display a dialog with more information or cancel and view the syslog for details.</string>
</dict>
</plist>
  1. 跟 Automator 一样替换图标文件即可。

封装依赖

之前做的这些更准确的说是包装而不是封装,因为我们的程序极度的依赖外部的文件,这个Application 打包给别人并不能做为独立应用直接使用。如果希望能做为独立应用还需要做以下工作:

程序内添加应用依赖

本文整体都是以 Apple 风格的Java程序,如果希望以Oracle风格构建Application可以查看universalJavaApplicationStub文档及源码。

Resources目录下创建Java目录。Demo.app/Contents/Resources/Java,然后把jadx所有依赖的jar包复制到这个目录下。 然后配置classpath为$JAVAROOT/\\*

打包JRE

如果需要把JRE一起打包进来用jlink(jdk9+) 或 jpackage(jdk8)更合适。 universalJavaApplicationStub的JAVA_HOME支持相对路径。在Info.plist内配置LSEnvironment并复制JRE到对应位置就可以了。

1
2
3
4
5
<key>LSEnvironment</key>
<dict>
<key>JAVA_HOME</key>
<string>Contents/Frameworks/jdk8u232-b09-jre/Contents/Home</string>
<dict>

macOS 10.15+ 可能会遇到的问题

从macOS 10.15开始,macOS 默认会阻止对受保护资源的访问,如用户的下载、文档或桌面文件夹,并显示一个安全对话框,用户必须接受该对话框才能被允许访问。 当在你的应用程序中使用javax.swing.JFileChooser时,它支持这些类型的安全对话框(有趣的是java.awt.FileDialog不支持!),你应该使用universalJavaApplicationStub脚本的编译二进制文件而不是普通的bash脚本。

二进制文件有两个不同的版本,根据自己的系统选择。 universalJavaApplicationStub-xxx-binary-macos-10.15.zip 的可能可以在 macOS 11 上使用, universalJavaApplicationStub-xxx-binary-macos-11.0.zip 肯定不能在 10.15 的系统上使用。

打开方式 Open With

如果希望在class、jar、java、zip等文件上点右键能出现该程序,只需要在Info.plist里添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>class</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/java</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleTypeName</key>
<string>Class File</string>
<key>LSIsAppleDefaultForType</key>
<true/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>java</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>text/plain</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleTypeName</key>
<string>Java File</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>jar</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/java-archive</string>
</array>
<key>CFBundleTypeName</key>
<string>Jar File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>war</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/java-archive</string>
</array>
<key>CFBundleTypeName</key>
<string>War File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>ear</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/java-archive</string>
</array>
<key>CFBundleTypeName</key>
<string>Ear File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>aar</string>
</array>
<key>CFBundleTypeName</key>
<string>Android archive File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>jmod</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/java-archive</string>
</array>
<key>CFBundleTypeName</key>
<string>Java module File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>zip</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/zip</string>
</array>
<key>CFBundleTypeName</key>
<string>Zip File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSIsAppleDefaultForType</key>
<false/>
<key>LSTypeIsPackage</key>
<false/>
</dict>
</array>

universalJavaApplicationStub-Info.plist https://gist.github.com/cnxobo/e53e0571c5165913e68c8db3600c7d29

dorado 接口安全风险

dorado 封装了Ajax请求,导致在权限处理上与传统Web项目还是有很大的区别。dorado下,暴露一个服务可以通过@Expose@DataProvider@DataResolver等注解。 比如,我想提供一个接口,参数为name, 返回值为"Hello " + name, 我只需要写一个Java方法,同时把类注册到Spring上下文,并在方法上标注@Expose

1
2
3
4
5
6
7
@Service
public class DemoController {
@Expose
public String hello(String name) {
return "Hello " + name;
}
}

像这样的一个接口,在任何一个dorado页面都可以通过构建一个AjaxAction,并配置service为beanId#方法名的形式 demoController#hello, 就可以调用这个方法。

1
2
3
4
5
6
new dorado.widget.AjaxAction({
service: "demoController#hello",
parameter: "xobo"
}).execute(function(result) {
console.log(result)
})

dorado封装了HTTP请求,所有的请求统一发给URL /dorado/view-service,然后由报文内容决定调用哪一个接口。 dorado 的这一特性导致,无法通过拦截URL的方式管理接口的权限,只能通过AOP的方式去拦截方法从而实现接口的管理。每个页面都会提供大量的接口,不同的页面可能会使用相同的接口。会导致接口权限的配置变得特别复杂。 从简化配置的角度,可以通过解析dorado view建立dorado接口与页面的映射,通过判断是否有权限访问接口所在的页面来判断能否访问接口。然后对需要更细粒度管理的接口做一个额外的配置。 基于此思路开发了 dorado exposedservice security https://github.com/cnxobo/dorado-exposedservice-security.git ,以解决dorado接口安全问题。

NUC8 黑果Monterey启动特别慢

NUC8 黑果Monterey启动特别慢

三星的某些型号的SSD,比如我的970 EVO plus,执行TRIM的操作特别的慢。而在APFS上,如果启用了TRIM,macOS会在启动的时候执行一次TRIM操作释放未使用的空间,就会导致启动速度特别的慢。 可以通过升级OpenCore到 0.7.9 及以上版本,然后设置SetApfsTrimTimeout值为0(默认为-1)关闭启动时的TRIM操作以提升启动速度,我的是从启动时间5分钟提升到20s。

macOS 12.0及以上版本 SetApfsTrimTimeout超时功能失效只有0禁用,及其值开启。

macOS的APFS

macOS的APFS针对SSD做了特别的优化,它会自动管理和优化SSD的存储空间,从而减少了对TRIM的依赖。所以macOS也就没有提供手动的命令来触发TRIM,只有系统启动或者发现新的设备连接的时候才会触发一次TRIM。 如果系统关机启动比较频繁的话,或者系统更新时需要频繁重启,建议SetApfsTrimTimeout设置为0,关闭开机时TRIM,以加速开机时间。如果常年不关机,可以保持默认值-1。

如何修改OpenCore

Shell

OpenCore配置是写在EFI分区的。所以要先看看EFI分区在哪里。 1. 找到EFI分区的IDENTIFIER。

1
diskutil list

会有一个类似下面的输出:

1
2
3
4
5
/dev/disk0 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *1.0 TB disk0
1: EFI EFI 209.7 MB disk0s1
2: Apple_APFS Container disk2 1000.0 GB disk0s2

在这个例子里,EFI分区的IDENTIFIER是disk0s1。

  1. 挂载EFI分区
1
sudo diskutil mount disk0s1
  1. 修改配置 配置文件位于/Volumes/efi/EFI/OC/config.plist。 搜索SetApfsTrimTimeout找到如下位置
1
2
<key>SetApfsTrimTimeout</key>
<integer>-1</integer>

把其中的-1改为0, 保存编辑器并退出。

  1. 卸载之前挂载的EFI
1
diskutil umount /dev/disk0s1

也可以通过 Hackintool 软件来挂载EFI分区。具体的操作步骤可以参考这个 NUC8黑苹果更新OpenCore引导过程记录 https://zhuanlan.zhihu.com/p/431713988

其他信息

我更新的OpenCore是从这里下载的 OpenCore for NUC8i5BEH [https://github.com/Jiangmenghao/NUC8i5BEH\](https://github.com/Jiangmenghao/NUC8i5BEH target=”_blank”)

reference

OpenCore Configuration https://dortania.github.io/docs/latest/Configuration.html SSD hall of fame needs new entries · Issue #192 · dortania/bugtracker https://github.com/dortania/bugtracker/issues/192

Spring Security 3 升级导致 Request method 'POST' not supported

一个上古项目由于安全原因需要升级依赖,其中Spring 版本需要由3升级到5。完成版本升级之后GET请求的接口都是正常的,POST请求的接口都会报Request method ‘POST’ not supported。

[AbstractHandlerExceptionResolver:199] - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method ‘POST’ not supported]

在debug跟踪代码,发现请求会在CsrfFilter里被转发到/然后报错。通过查阅Spring迁移文档3to4发现,Spring Security4开始默认启用Csrf。由于之前项目并不支持Csrf,导致权限框架把请求拦截下来。给项目配置禁用Csrf就可以了。<csrf disabled="true"/>

1
2
3
4
<http>
...
<csrf disabled="true"/>
</http>

JavaScript Number 四舍五入并保留两位小数

Math.round()

JavaScript 提供了一个函数Math.round() 来做整数的四舍五入。

1
2
console.log(Math.round(0.4), Math.round(-0.4),Math.round(0.5), Math.round(-0.5), Math.round(0.6), Math.round(-0.6));
// 预期输出: 0 -0 1 -0 1 -1

Math.round() 准确的定义是把参数转化为最接近的整数,如果与相邻两个整数距离相等那么返回较大的整数。在正数情况下符合日常理解的四舍五入的行为,在负数情况下更像是五舍六入。 例如, -0.5,距离相邻两个整数0, -1 距离一样,返回较大的整数 0。-0.6 距离-1更近,返回-1。

使用Math.round()先乘100再除100保留两位小数 ❌

Math.round() 只能处理整数,可以通过乘以100把小数点左移两位,然后再通过Math.round()完成整数的四舍五入,然后再把小数点右移两位(x1/100)。

1
2
3
4
5
function round2(num) {
return Math.round(num * 100)/100;
}
console.log(round2(3.1415926), round2(3.145926))
// 预期输出: 3.14 3.15

看起来是好的,再多一点测试。

1
2
console.log(round2(1.005))
// 预期输出: 1.01 实际输出: 1

Number.EPSILON ❌

浮点计算会丢精度可以考虑加一点点数值来修正。加一个Number.EPSILONNumber.EPSILON是一个常量值,最小的大于1的浮点数和1的差。

Chrome下 Number.EPSILON =2.220446049250313e-16

1
2
3
4
5
function round2(num) {
return Math.round((num+Number.EPSILON) * 100)/100;
}
console.log(round2(3.1415926), round2(3.145926), round2(1.005))
// 预期输出: 3.14 3.15 1.01

再多一点测试

1
2
console.log(round2(5.015))
// 预期输出: 5.02 实际输出: 5.01

没的救了,换个其他方法

toFixed ❌

toFixed 参数是小数点后保留几位小数,返回的是string。参数默认为0。

1
2
console.log(12.345.toFixed(), 12.345.toFixed(1), 12.345.toFixed(2))
// 预期输出: 12 12.3 12.35
1
2
3
4
5
function round2(num) {
return parseFloat(num.toFixed(2));
}
console.log(round2(5.015))
// 预期输出: 5.02 实际输出: 5.01

一模一样的失败,还有个toPrecision 跟 toFixed 一样的失败。

toPrecision ❌

toPrecision 参数一共有几位有效数字,包括整数和小数位。如果没填参数,就等同于Number.prototype.toString()

1
2
console.log(12.345.toPrecision(), 12.345.toPrecision(1), 12.345.toPrecision(2), 12.345.toPrecision(3), 12.345.toPrecision(4))
// 预期输出: 12.345 1e+1 12 12.3 12.35

toPrecision & round

浮点数在计算时会丢失精度,可以通过JavaScript有效数字位数是17, 可以通过降低精度toPrecision(15)完成精度的修正。差不多算是错错之后看起来为正。

1
2
3
4
5
function round2(num) {
return Math.round((num* 100).toPrecision(15) )/100;
}
console.log(round2(5.015), round2(1.005), round2(39.425))
// 预期输出: 5.02 1.01 39.43

Exponential notation

任何一个实数都可以写成 $x * 10^y$ 的形式,用指数表示法就是 xEy, eE 在浮点数字中代表指数。

1
2
console.log(1e2==100, 1e-2=0.01)
// 预期输出: true true

通过指数表示法,避免对浮点数做计算的方式来调整小数点位置,防止精度损失放大,然后配合round完成四舍五入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function roundNumber(num, scale) {
if(!("" + num).includes("e")) {
return +(Math.round(num + "e+" + scale) + "e-" + scale);
} else {
var arr = ("" + num).split("e");
var sig = ""
if(+arr[1] + scale > 0) {
sig = "+";
}
return +(Math.round(+arr[0] + "e" + sig + (+arr[1] + scale)) + "e-" + scale);
}
}
function round2(num) {
return roundNumber(num, 2);
}
console.log(round2(5.015), round2(1.005), round2(39.425))
// 预期输出: 5.02 1.01 39.43

lodash

Lodash的round实现原理类似。 _.round(number, [precision = 0])

1
2
console.log(_.round(1.005, 2))
// 预期输出: 1.01

为什么一个简单的四舍五入并保留两位小数这么麻烦

0.1 在浮点数里并不存在

当我们声明一个小数时,比如0.1其实它并不是0.1它其实是0.10000000000000000555111512312578270211815834045410。编程语言在默认打印的时候会降低精度再打印,让他看起来是0.1

1
2
3
4
5
6
7
8
9
10
11
console.log(0.1.toFixed(50))
// 输出: 0.10000000000000000555111512312578270211815834045410

console.log(0.2.toFixed(50))
// 输出: 0.20000000000000001110223024625156540423631668090820

console.log(0.5.toFixed(50))
// 输出: 0.50000000000000000000000000000000000000000000000000

console.log(5.015.toFixed(50))
// 输出: 5.01499999999999968025576890795491635799407958984375

这样就能看出为什么保留两位小数有时候好使,有时候就不好使。

浮点的标识方式

浮点数是采用科学计数法来表示一个数字的,常见的编程语言的浮点数都是基于IEEE754标准。 IEEE754 浮点数基数固定是2,所以还需要关注符号、尾数和指数三部分。 sign 符号, 表示正负号 exponent 指数,表示次方数 mantissa 尾数,表示精确度 $(-1)^s_2^e_1.mantissa$ 由于基数是2,也就无法表达出我们常见的小数,例如: 0.1, 0.2, 0.05。对于无法表达的数只能使用近似值,也就导致 0.1 + 0.2 = 0.30000000000000004。 只要是浮点表示的小数,早晚会遇到精度问题。 对于需要精准小数计算的还是要用Math.jsdecimal.js这种专门的库来计算。

更多浮点相关的知识

15 张图带你深入理解浮点数 IEEE-754浮点数对应二进制转化 IEEE-754浮点数可视化 IEEE-754浮点数可视化float-toy