Java
Java
java 缺点 - 暴论
入门 java,远离 java。
- DBeaver 烂,JB 家一堆烂,Minecraft 烂,sonarlint 烂,我接触到的 java 写成的玩意总能给我带来不悦。
- 作为带 GC 的语言,Java 的性能一般(强类型打不过弱类型);与其他 GC 语言相比,Java 编写的复杂度又更高。
- java 社区相较而言也死气沉沉,到处是 java 8(今夕是何年)。
- java 学到后期全是业务框架,不好玩,没意思。对于喜欢做小项目的我,java 的业务抽象反而是额外的复杂度,也不能让代码可读性和易用性更强。
哦,顺带一提,Android 开发建议直接 kotlin,Google 官方推荐的语言,并且全兼容 java。确实比 java 有意思多了。
然而现在国内的 java 就业需求仍然是最高的,如果为了就业的话就不得不学了。
开发环境
jdk
Windows:使用 scoop,或者去官网下载。Linux:不多说了。
scoop bucket add java
# 然后根据你需要的版本安装对应 jdk。
scoop install openjdk22
scoop install corretto8-jdk
scoop install liberica8-full-jdk几个不同提供方:
- corretto* 是 Amazon 维护的免费、生产级 OpenJDK 8 发行版,支持周期长,包含安全修复。
- temurin* 是社区和其他厂商维护的版本,更改少点。
- openjdk* 是 oracle 自家的原味版本。注意 openjdk 已经没有 java 8 了。
如果你是为了兴趣学习,安装最新版本 jdk;如果是工作,根据不同要求可以选择安装 java 8 或 java 17。
顺带骂一下,scoop 这个 java 仓库做的真 tm 烂,想下载哪个玩意只能进 bucket 文件夹看源码。
IDE
20260903,JetBrains 宣布推出 Java and Kotlin by IntelliJ IDEA LSP,可以在 vscode 上使用。
该插件直接杀死了比赛。从此你不再需要各种调优、群魔乱舞的插件,只需要一个 Java and Kotlin by IntelliJ IDEA 即可畅游 Java 项目,享受极速跳转。
虽然经常听到 IDEA 内存占用大的声音,但是 Java and Kotlin by IntelliJ IDEA 其实只是 IDEA 的一个 core,没有乱七八糟的功能,内存占用在大项目上其实比 JDT LS 还要低上许多。简直是对 JDT LS 的全方位暴打。
注意
202609 之后,不再推荐使用 Language Support for Java(TM) by Red Hat。
如果只是写一些小项目,比如跑跑学校的课程,并且不需要多人协作,vscode 是完全够用的。
对于大型项目,强行使用 vscode 可能需要付出一些代价,但也绝非不可能,我一直在研究这方面的解决方法,并且有一些小心得。
还没学会 JVM 调优,要先学会 Language Support for Java(TM) by Red Hat 调优了。。
扩展
分析器:
- 在 vscode 扩展商店搜
java,直接装 Language Support for Java(TM) by Red Hat 即可,这是最老牌、兼容性最好的 java 插件,用来分析大项目也是没啥问题的。- Red Hat 这个插件在跳转的时候会比 idea 慢一些。nodejs 性能本来就差,然后有时候的跳转没法命中提前分析出的缓存,可能需要当场分析,因此比较慢。注意不要用 Hybrid 模式,用 Standard,这样跳转会更快一些,后续的配置里有。
- 不要装 Apache NetBeans,这玩意不是生产可用的。
运行/调试器:
- 对于小型项目,例如单文件,直接
javac xx.java生成.class字节码。java xx执行程序就行。 - 对于中大型项目,安装微软的 Debugger for Java。然后写一个
.vscode/launch.json,具体内容可以从 idea 导出 XML 配置后,再让 AI 生成。然后就可以在左侧边栏找到 debugger 图标,运行或调试了。这里是我的 example。{ "version": "0.2.0", "configurations": [ { "type": "java", "name": "XXX", "request": "launch", "mainClass": "com.xxx.yyy.zzz", "projectName": "yyy", "shortenCommandLine": "jarmanifest", "vmArgs": "-Dfile.encoding=UTF-8 -Dxxx.xxx=xxx", "env": { "k": "v" } } ] }- Debugger for Java 还有一个非常好用的地方是可以 Hot Code Replace,也就是调试过程中本地修改的代码可以直接生效,无需重启后端。这对于一个光是启动就要两分钟的 java 项目来说,是非常节省时间的。
其他可选扩展:
- Mybatis 跳转:Mybatis Helper - Greenplumwine,可以在 Mapper java 和 xml 之间互相跳转。这玩意 fork 了好几版,这个是目前还有人在维护的一版。
- Test Runner for Java,如果你的项目有用到单测的话可以装一下,还是非常方便的,不用手写
launch.json。跑完单测,stdout 输出的结果在 调试控制台 这个 tab 下。 - SonarQube (原 SonarLint):Language Support for Java(TM) by Red Hat 的静态分析能力差了 idea 一大截,所以如果希望自己的代码可以有更严格的检查,可以使用 SonarLint。这玩意跑了一个自己的静态分析器,跟 redhat 的 Eclipse 分析器同时进行 double check,对性能有一定影响。推荐真正有需求的使用。
- 而且很多本该能推出来的 auto fix,SonarLint 也推不出来。
- 看看 sonarqube 的仓库吧,一个连 issue 都关掉的产品能做得多好?
- SonarLint 还有瞬间高 CPU 占用的问题,即使 vscode 状态里显示其没有在扫描。
- SonarLint 默认不扫第三方库,也就是你写一个
org.apache.commons.collections4.CollectionUtils.isEmpty判空之后,它还会报 Potential null pointer access,纯废物。
相关配置
环境相关配置也非常恶心,Language Support for Java(TM) by Red Hat 插件、项目本身、gradle 需要的 java 版本可能都不同,但是新手并不知道哪些配置读的是 JAVA_HOME,哪些配置读的是 vscode settings,哪些配置是继承的其他配置等等。
一般来说 PC 上可以安装两个 java,一个是最新的 openjdk 用来跑 vscode 插件(如果不用 vscode 就没必要了),一个是项目要求的 java 版本,例如我们的是 liberica8-full-jdk。对于 scoop 安装的 java,openjdk 路径指的是 <scoop>/apps/openjdk/current,其他同理。
对于 gradle 版本,我建议使用自己安装的而不是项目里的。否则启动 gradle daemon 时可能遇到很多奇奇怪怪的问题,例如这个。gradle 的不同版本可以在 scoop 里安装,至于配置的话可以放到项目下 .vscode/settings.json,方便每个不同项目使用不同版本。
关于项目调优:最重要的配置项就是 java.jdt.ls.vmargs 和 java.server.launchMode 两项。java.jdt.ls.vmargs 决定 java 插件的运行条件,对于个人开发需要经常切分支的场景,需要调高 GC 频率和其他优化,避免顶爆内存。java.server.launchMode 决定 java 插件的工作模式,修改后可以保证尽快分析与渲染。
这是我的项目配置:
{
"java.cleanup.actions": [],
"java.format.enabled": false, // 参考 formatter 章节
"java.import.gradle.wrapper.enabled": false, // 必须在这里设为 false,后面的两个配置才有用!!
"java.import.gradle.home": "C:/Users/xxx/scoop/apps/gradle4/current", // 设置 gradle 位置。对于 java8 项目应该使用 gradle4。
"java.import.gradle.java.home": "C:/Users/xxx/scoop/apps/liberica8-full-jdk/current" // gradle 本身使用的 jdk,需要和 gradle 版本匹配。这里使用 java8。
}我的用户配置:
{
"java.autobuild.enabled": true,
"java.codeGeneration.addFinalForNewDeclaration": "all",
"java.compile.nullAnalysis.mode": "automatic",
"java.completion.chain.enabled": true,
"java.completion.filteredTypes": [
// 不要自动补全这些玩意
"java.awt.*",
"com.sun.*",
"jdk.*",
"sun.*",
"org.omg.*",
"java.sql.Date"
],
"java.completion.importOrder": [
// 设置自动 Import 的分组顺序
"java",
"javax",
"org",
"com"
],
"java.completion.postfix.enabled": true,
"java.configuration.runtimes": [
{
"default": true,
"name": "JavaSE-25",
"path": "C:/Users/xxx/scoop/apps/openjdk/current"
},
{
"name": "JavaSE-1.8",
"path": "C:/Users/xxx/scoop/apps/liberica8-full-jdk/current"
}
],
"java.configuration.workspaceCacheLimit": 30,
"java.debug.settings.hotCodeReplace": "auto",
"java.debug.settings.vmArgs": "-Xms2g -Xmx4g -XX:+UseG1GC -XX:+UseStringDeduplication -XX:MinHeapFreeRatio=15 -XX:MaxHeapFreeRatio=30 -XX:+ExplicitGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000", // JDK 17 的推荐参数(使用 G1 GC)
"java.eclipse.downloadSources": false, // 禁止自动下载 Maven/Gradle 依赖的源码
"java.implementationCodeLens": "methods",
"java.import.exclusions": [
"**/node_modules/**",
"**/.git/**",
"**/target/**",
"**/build/**",
"**/.vscode/**",
"**/.idea/**"
],
"java.import.gradle.enabled": true,
"java.inlayHints.parameterNames.enabled": "all",
"java.inlayHints.variableTypes.enabled": true,
"java.jdt.ls.appcds.enabled": "on", // JVM 类数据共享存档
"java.jdt.ls.java.home": "C:/Users/xxx/scoop/apps/openjdk/current",
"java.jdt.ls.javac.enabled": "off", // 使用 ecj,更快,并且允许错误
"java.jdt.ls.vmargs": "-Xms2g -Xmx8g -XX:+UseZGC -XX:+ZGenerational -XX:+UseStringDeduplication -XX:ZUncommitDelay=30", // JDK 25 的推荐参数(使用 ZGC)
"java.maven.downloadDependencies": false,
"java.project.importOnFirstTimeStartup": "interactive",
"java.project.resourceFilters": ["node_modules", "\\.git", "target", "build", ".vscode", ".idea"],
"java.quickfix.showAt": "problem",
"java.referencesCodeLens.enabled": true,
"java.server.launchMode": "Standard", // 非常重要!大项目一定要开 Standard,不然默认 Hybrid 跳转定义要慢死
"java.sharedIndexes.enabled": "on" // 跨 Workspace 共享符号索引
}formatter
使用 vscode 做 java 大项目的一大痛点是 formatter。别人都在用 idea 进行格式化,而 idea 的 formatter 目前没有可用的第三方实现,如果用自己的 formatter,代码风格和别人的无法兼容。
- 目前来说,和 idea formatter 兼容性最高的还是原生 Language Support for Java(TM) by Red Hat,但是需要让使用 idea 的其他开发把 code format 导出为 Eclipse XML Profile,然后在 vscode 插件里指定。不过即使这样也没法完全复刻 idea 的 formatter,只能做到相对来说风格相似,仍会有一些行为是不一致的。
- a-havrysh/vscode-intellij-code-formatter 是个新代码库,到我写出这段文字的时候该仓库还是 0 star。
看到 commit 信息里有名为 claude 的菊花我就浑身难受,试用了下,果然不出意外地难用,即使我指定了 idea 导出的 XML 格式化文件,它的格式化行为也和 idea 是不一致的。
然后我想出了一个最终邪道:通过 commit hook 直接调用 idea 进行 format,本质上是起了个无窗口 idea。
注意执行这个脚本的时候 idea 不能正在运行,因为同一时间只能有一个 idea 实例。由于 idea 在 format 完还会干很多别的事,例如加载插件等,所以我在 format 结束后就立刻 kill 掉了。
#!/usr/bin/env python
import os
import platform
import subprocess
import sys
import time
STYLE_XML_PATH = r"C:\programs\work\XXX.xml"
IDEA_BIN_PATHS = {
"Windows": r"C:\software\IDEA\bin\format.bat",
"Darwin": "/Applications/IntelliJ IDEA.app/Contents/bin/format.sh",
"Linux": "/opt/intellij-idea/bin/format.sh",
}
ALLOWED_REPOS = [
"your_folder_name"
]
def get_platform_idea_bin():
system = platform.system()
path = IDEA_BIN_PATHS.get(system)
if not path:
print(f"[Error] Unsupported OS: {system}")
sys.exit(1)
if not os.path.exists(path):
print(f"[Error] IDEA formatter not found at: {path}")
print("Please update the path in 'idea_format.py' to match your installation.")
sys.exit(1)
return path
def get_staged_java_files():
try:
# --diff-filter=ACMR: Added, Copied, Modified, Renamed
cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"]
result = subprocess.check_output(cmd).decode("utf-8")
files = [f.strip() for f in result.splitlines() if f.strip().endswith(".java")]
return files
except subprocess.CalledProcessError:
print("[Error] Failed to get staged files.")
sys.exit(1)
def main():
if not os.path.exists(STYLE_XML_PATH):
print(f"[Error] Style XML not found: {STYLE_XML_PATH}")
sys.exit(1)
files = get_staged_java_files()
if not files:
sys.exit(0)
idea_bin = get_platform_idea_bin()
print(f"Found {len(files)} staged Java files.")
print("Running IntelliJ IDEA Formatter (this may take a few seconds)...")
abs_style_path = os.path.abspath(STYLE_XML_PATH)
cmd = [idea_bin, "-s", abs_style_path] + files
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="gbk" if platform.system() == "Windows" else "utf-8",
errors="ignore",
)
formatted_success = False
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line_content = line.strip()
print(f" [IDEA] {line_content}")
if (
"file(s) formatted" in line_content
or "files formatted" in line_content
):
print(
"Formatting detected. Killing IDEA process to skip plugin errors..."
)
formatted_success = True
time.sleep(0.1)
process.terminate()
break
if "Only one instance" in line_content:
print("\nError: IDEA is running. Please close it first.")
process.kill()
sys.exit(1)
if process.poll() is None:
process.kill()
if not formatted_success:
pass
except Exception as e:
print(f"[Error] Execution failed: {e}")
sys.exit(1)
print("Re-staging files...")
subprocess.check_call(["git", "add"] + files)
if __name__ == "__main__":
repo_dir = os.path.basename(os.getcwd())
print(f"Current directory: {repo_dir}")
if repo_dir not in ALLOWED_REPOS:
print(f"repo not in {ALLOWED_REPOS}, skipping formatting for {repo_dir}")
sys.exit(0)
main()但是 idea 还有个好用的功能是仅格式化有修改的代码行,如果你的 code base 本身并没有 fully formatted,但是又想对自己更改的内容开启格式化,这个就更难办了。在被骂过一次以后我又改进了一下脚本。
贴到 gist 上了,这里地方太小写不下.jpg
但凡有半点选择,我都不会用 idea,因为它作为一个编辑器实在是太烂了。。。 被恶心了十几天以后,我最终还是选择了 vscode。vscode 虽然比较弱鸡,但它不会恶心我。
idea 有社区版,这个是免费的,不需要破解,理论上也够用,没必要给 jb 送钱。
我习惯了 vscode,我比较希望将键位等都改成 vscode 上的习惯。idea 可以导入当前计算机上已有的 vscode 配置,不过算是聊胜于无。。这里有一些我的配置心得。
- 因为 jb 家的东西都差不多,之前配 Android Studio 的经验也可以套用一点。
- 继续删除/禁用那些没用的插件。idea 的社区版会预装很多 ultimate 的插件,然后不让你用。。
- 编辑器 > 常规 > 编辑器标签页,勾选 标记已修改。非常重要,被坑了几次。。。
- 调整 KeyMap:前提是把 vscode 的 KeyMap 同步过来。
- 取消绑定所有 Ctrl + w,只保留
窗口 -> 编辑器标签页 -> 编辑器关闭操作 -> 关闭标签页。否则在终端里使用 Ctrl + w 会关闭终端。 - 即使把 vscode 的 vim 插件同步过来,vscode 的 vim 配置也不会同步到 ideavim。很多键位也是需要改的,比如我保留了许多编辑器自己的行为而不是 vim 行为。
- 取消绑定所有 Ctrl + w,只保留
- 配置 ideavim。
" 双引号是注释 set nocompatible " 关闭vi兼容模式 set clipboard+=unnamedplus " 复制粘贴时同时复制到剪贴板 set incsearch " 搜索时实时高亮 set hlsearch " 高亮搜索结果 set ignorecase " 搜索忽略大小写 set smartcase " 如果搜索包含大写,则不忽略大小写 vnoremap <C-c> "+y " 将 Visual 模式下的 Ctrl+c 映射为复制到系统剪贴板 " 删除相关 nnoremap x "_x vnoremap x "_x snoremap x <C-g>"_x nnoremap d "_d vnoremap d "_d snoremap d <C-g>"_d nnoremap D "_D " select mode 相关 set keymodel^=startsel set selectmode+=mouse,key set idearefactormode=keep vnoremap <BS> "_c snoremap <BS> <C-g>"_c vnoremap <Del> "_c snoremap <Del> <C-g>"_c- ideavim 其他常用操作:
gd是 go definition,就是鼠标 ctrl + 左键单击。<C-o>是返回,<C-i>是前进,相当于两个鼠标侧键。
- ideavim 其他常用操作:
- 为所有文件开启软换行。
- 关闭 分支切换时还原工作区,没啥必要,又没开几个面板
如果 idea 以管理员运行,则插件也会获得管理员权限,而插件的行为不是我们能控制的,所以最好不要以管理员运行 idea。
gradle
安装
全局安装 gradle 的话 scoop install gradle 一行完事。如果有需要固定版本还可以 scoop install gradle4、scoop install gradle7 等。
也可以用项目里配置的 gradle 锁定版本,好处是和别人运行起来完全一致,不过可能导致多个 gradle 版本共存。如果运行 ./gradlew build,脚本会帮你下载对应版本的 gradle,而且下载得还贼慢。。。不会做下载器就别做。 此时需要自己下载,然后把整个 zip 放到 C:\Users\<用户名>\.gradle\wrapper\dists\gradle-版本-bin\<一串随机乱码>\ 下。
配置
gradle 的配置还是很重要的,好的配置可以大幅加速编译、避免内存占用。推荐将配置放在 ~/.gradle/gradle.properties 作为全局配置,所有项目生效。
- 避免多开 gradle 进程:gradle 复用同一个 daemon 的前提是 gradle 版本、JDK 路径、JVM 参数完全一致。需要确保不同项目之间使用的是同一份配置(全局配置),编辑器里不要配一些可能修改 gradle/JVM 参数的东西。
org.gradle.daemon=true org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC org.gradle.caching=true org.gradle.parallel=true org.gradle.configuration-cache=true org.gradle.configuration-cache.parallel=true org.gradle.vfs.watch=true org.gradle.configureondemand=true - 平常跑的时候就直接用 gradle xxx,不要带其他参数。不要用
--no-daemon,那个很慢。
杂
- gradle clean 只会清理项目下的
build/文件夹。如果你使用 vscode 开发可能还会有bin/文件夹,这是 Language Support for Java(TM) by Red Hat 进行静态分析的产物,跟 gradle 没有关系。
API Client
写后端,有一个 API Client 几乎是必须的。它的基础功能是导入从浏览器复制的 curl,然后你可以修改一些参数或者 header 再发出请求,进行一些功能验证。
推荐使用 bruno,postman 的开源替代。postman 吃相太难看了。
其他注意事项
- vscode 按住 ctrl 跳转,跳到了基类实现。但是实际的实现根据传入的对象,可能是基类实现也可能是子类实现。所以开启一下 implementation lens 是比较重要的。不过话又说回来,这个 lens 的加载巨慢,因此可能也是没什么用的。
{ "java.implementationCodeLens": "methods", "java.referencesCodeLens.enabled": true }
语言基础
判断
- java 的 if 语句中只能放入 boolean 或其包装类。
循环
JDK 5 引入了 for (Type var : iterable) {} 的循环。
- 循环中发生的 copy,对于对象是引用 copy,对于基本类型是值 copy。
var
Java 10 语法糖,类型推断。like C++: auto, but not so powerful.
exception
java 错误处理挺不错的,至今也有人认为 java exception 优于 rust
Result<T, E>.
函数可以声明可能抛出的异常,则 throw exception 后会直接跳出这个函数。只有受检异常(非 RuntimeException)必须声明 throws 抛出;RuntimeException 和 Error 是隐式抛出的,不需要声明。
class X1Exception extends Exception{}
class X2Exception extends Exception{}
void t() throws Exception{
throw new X2Exception("...");
assert unreachable == true;
}
// ... main
try { t(); } catch(X1Exception | X2Exception e) {
System.out.println(e.getMessage());
}java 7 之后还可以使用 try-with-resource 以自动释放资源。
断言
使用非常简单,assert expression。默认不启用检查,需要加入 JVM 参数 -ea 才会触发运行时检查:java -ea Main.java。
关键字
- final:不可变,immutable。不需要同步,性能高。
- native:表示函数是外部的,类似 C/rust 的 extern。
- record:java 14 语法糖,快速创建一个拥有不可变成员的类,自带 toString 等。
- volatile:用于多线程的关键字,强制数据在内存和 CPU cache 里同步,所有线程看到的变量都是最新的。volatile 不等于加锁。
- synchronized:互斥锁,同一时间只能有一个线程访问此资源。
数据结构
- 创建临时数组:
- >= java 9:使用
List.of,创建不可变的数组。 - java 8:使用
Arrays.asList。
- >= java 9:使用
- 类似的,创建临时 Map:
- >= java 9:使用
Map.of。 - java 8:使用
com.google.common.collect.ImmutableMap,用法跟Map.of类似。否则就需要用 Stream + collect 或者手动添加元素创建。
- >= java 9:使用
- 动态数组:
- Arraylist 和 Vector,后者是线程安全的,更慢。
private ArrayList<account> accounts = new ArrayList<>(List.of(new account("admin", "admin")));
- Arraylist 和 Vector,后者是线程安全的,更慢。
- Hashmap:哈希表,不多说。
- LinkedHashMap:有序的 HashMap,按照添加顺序
面向对象
基础的就不说了。这里随便记一些。
- 类代码块:class 里可以写一段
static { ... }静态代码块,内部的代码会在类初始化时执行(构造之前、访问 static 成员之前),有一点点像 Rust LazyLock。如果写{ ... }就是实例代码块,每次创建实例时都执行。 - 如果要处理 interface,downcast 到不同的 class:
- java 8 只能 if (xxx instanceof xxx) 判断然后强转。
- java 21 可以 switch。
- 子类可以定义相同的变量“遮掩”父类的变量。两个变量同时存在,访问父类变量需要用
super.,或者 cast 到父类再访问。 - clone:java 没有原生的 deep clone,很多实现都还在用 json 序列化/反序列化来做 deep clone,实在是丑陋啊。
包装类
基本类型例如 bool,包装类 Boolean。
- 一般尽可能用基本类型,性能好,还有非空保证。但有时候不得不用包装类,比如需要 nullable,或者泛型等。
- java 中只要是包装类,就有可能是 null,一旦在逻辑判断中直接当做基本类型用(自动拆箱),就会 NPE。比较难防。
- 这时候需要一些额外操作,例如使用第三方包 org.apache.commons.lang3.BooleanUtils。就是一个默认会判空的 Boolean 判断条件。
lambda
java 8 引入的匿名函数。(parameter1, parameter2) -> expression,expression 也可以是一个花括号 code block。
- lambda 捕获的局部变量必须是 final 或等效 final(effectively final)的变量。
泛型
泛型在实现上基于类型擦除,是因为泛型出现时大量的代码没有使用泛型,需要保证和老代码的可交互性。这意味着泛型只在编译期存在,运行期不存在,使用 Jackson 等进行反序列化的时候需要添加 TypeReference。
- 泛型不支持基本类型,只能用包装类。
- 泛型不可变,比如我不能将
List<T>转为List<I>,即使 T implements I。必须新建一个 List,然后强转过去(构造函数强转)。
Stream
Stream (Java 8) 是 java 中很重要的一个概念,可以理解成迭代器。类比 Rust iterator 或 C++20 的 std::ranges。Stream 提供了一组函数式链式操作。
刚发现的写的不错的参考。
基本操作
- 连接流:
Stream.concat(stream1,stream2) - 映射:
map - 过滤:
filter - 元素操作:
forEach(lambda)
常用方法
var list = Stream.of(1,2,3).collect(Collectors.toCollection(ArrayList::new)); // 为 Arraylist 赋值
list.stream(); // Arraylist 转流
Stream.of(1,2,3).mapToInt(Integer::intValue).max().getAsInt(); // 计算最大值
// 对于 Arraylist, 更好的做法是 Collections.max(list)
Stream.of(1,2,3).mapToInt(Integer::intValue).average().getAsDouble(); // 计算平均值
Stream.of(1,2,3).sorted(); // 排序(升序)
Stream.of(1,2,3).sorted(Comparator.reverseOrder()); // 排序(降序)
Stream.of(1,2,3).anyMatch(x -> x == null); // 任一元素满足。使用 allMatch 是所有元素满足。collect(Collectors.toMap(...))是超级大坑,toMap 不允许 Value 为 null,否则会直接报 NPE。如果可能为空,需要用collect(HashMap::new, (m, v) -> m.put(v.getId(), v.getNote()), HashMap::putAll);。
Intstream
以这个结构为例了解其用法。
var temp1 = IntStream.rangeClosed(1, 3); // 生成闭区间流 1,2,3
var temp2 = IntStream.range(1, 3); // 生成左闭右开区间流 1,2
var temp3 = temp1.boxed(); // Intstream 转为 StreamparallelStream
Java 的 List 只需要调用 .parallelStream() 就可以自动转换为并行多线程处理(CPU bound 的线程池),这个思想实在是太领先了。
Optional
Optional 的作用是强制调用方处理空值。
Optional.ofNullable(A).orElse(B); // 最常用的 Optional 用法,相当于 A != null ? A : B
Optional.ofNullable(A).orElseGet(() -> B.fun()); // 如果 else 里是一个比较复杂的函数,使用 orElseGet 可以延迟执行该函数,提高性能
Optional.of(value) // 创建,value 不可为 null
Optional.ofNullable(null) // 创建
Optional.ofNullable(123).ifPresent(u -> System.out.println(u)); // 操作值
Optional.ofNullable(123).map(u -> u + 2); // 映射
Optional.ofNullable(123).filter(u -> u < 150); // 映射java 9 添加了一些 java 8 没有的 Optional.or() 等链式处理为空的逻辑,还包括了 ifPresentOrElse() 当成 rust 的 match 用。
由于 java 泛型不能为基本类型,这里的 Optional 都会自动装箱。为了避免装箱性能损耗,java 8 还额外提供了 OptionalInt、OptionalLong 和 OptionalDouble 三个类型,建议使用。(没有 OptionalBoolean,因为 Boolean 本身就可以表示可空)
当然,在没有强制性要求处理空值的地方(内部调用等),或者对性能敏感的地方,用 @Nullable 注解即可。如果是 Spring 开发场景,优先使用 org.springframework.lang.Nullable。
注解
java 5 (2004 年)引入了注解,非常早。注解现在已经成了业务开发中不可或缺的一部分。注解可以在编译期或运行期被读取,并且改变代码的行为,跟 rust 的 proc macro 还是有一些区别的。之后的 业务内容 会更详细地介绍一些框架注解。
- java 注解的属性值必须是编译期常量。
- java 的注解相当混乱,同一个注解可能有一堆 package 提供;不同 package 提供的注解功能不同,不能混用。
序列化
序列化可以将一个 class 进行结构化表示,Jackson 或其他库可以利用这个结构表示将类转换成其他东西,例如 json。
class account implements Serializable {} // 继承接口实践中,建议为每一个 implements Serializable 的类都添加一个 private static final long serialVersionUID 成员,用于序列化的兼容检查。反序列化时,如果该 id 不匹配则会抛出异常。
Jackson
最常用的 json 序列化库,Spring 默认。
- 自定义序列化函数:使用
@JsonValue自定义序列化过程,使用@JsonCreator自定义反序列化过程。对于 enum,这个是比较常用的。@JsonCreator允许使用 mode 限制反序列化的来源,例如@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)限定只能从 json 对象字符串里反序列化,@JsonCreator(mode = JsonCreator.Mode.DELEGATING)就是非 json 对象字符串(普通字符串)里反序列化。
- 忽略
- Jackson 默认忽略 static 和 transient 字段。不需要担心
serialVersionUID被序列化。 - 忽略单个字段:
@JsonIgnore修饰字段。 - 忽略多个字段:
@JsonIgnoreProperties({"xxx", "yyy"})修饰类。 - 忽略多余字段:
@JsonIgnoreProperties(ignoreUnknown = true)修饰类。
- Jackson 默认忽略 static 和 transient 字段。不需要担心
- 默认值:直接
private xxx = xxx即可。 - 将成员类“展平”,类似 rust
#[serde(flatten)]:@JsonUnwrapped - 如果希望一个 json object 根据其 keys,可以反序列化到两种不同的 class:使用一个 interface 和 deduction。
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @JsonSubTypes({ @JsonSubTypes.Type(value = Class1.class), @JsonSubTypes.Type(value = Class2.class) }) public interface ObjectItem {} - (Java 8+) Jackson 可以配合 lombok,把
@JsonCreator注入到 lombok 生成的函数上,例如@AllArgsConstructor(onConstructor_ = {@JsonCreator})
一些坑:
- 不要定义
get、is开头的非 getter 函数!否则会当成 getter 进行序列化,最终结果和预期不同。(之前不写 java 的很容易被坑到)
swing
java GUI。原生自带,无需下载其他组件,无需构建系统,单文件就能跑。但是在前端设计上,swing 是不合格的,纯靠布局真的是非常噩梦的体验,没人用。
要不是学校的 java 课要用 swing 做东西,我也不会去用它。
默认折叠,点击展开 swing 相关内容。
组件
- JPanel:理解为容器
- JLable
- 设置字号/字体/效果:
lable.setFont(new Font(Font.SERIF, Font.BOLD, 18));
- 设置字号/字体/效果:
- JPasswordField:密码输入框。
getText()不安全会被警告,getPassword()则返回 char[] 需要自行处理。
需要注意,在 remove 后 add 组件,需要重绘。
panel.revalidate();
panel.repaint();事件
>=Java 8: 请使用 lambda 函数而不是将所有窗口事件在一个函数中处理。
button.addActionListener(e -> {
...
});业务内容
前面的基础结束,现在是业务内容。工作中的 java 业务一般有着一套固定的流程和约定,这里又是一大堆东西。
java 业务开发一般遵守分层架构:Controller 层是对外交互的接口,Service 层是最主要的逻辑区域,Mapper 层是数据库的交互。
基础名词
- Bean 是一种约定的特殊 class,所有属性 private,通过 getter setter 读取/修改,有一个无参构造函数。各种框架可以根据这样的约定来操作这个类及其实例。
- DTO(Data transfer object)、DO(data object)都相当于 python dataclass,二者的区别是用途:DO 一般是进 DB 的,DTO 是跟其他服务交互的。
- DI(Dependency Injection):依赖注入,之后 Spring 会提到。
- AOP(Aspect-Oriented Programming):面向切面编程,类似注入的概念,在运行时将一段逻辑注入到对象里。业务层对 AOP 是无感知的。Spring 的 AOP 是动态代理,在运行期生成一个代理对象。AspectJ 主要是直接修改字节码,功能更强,可以切入构造函数、静态方法、私有方法。
Lombok
Lombok 是一个业务开发必备库,作用是在编译时通过注解自动生成代码。
常用的注解有:
@NonNull:可以修饰入参和出参对象。如果为空会 NPE。- Lombok 的
@NonNull是运行时检查,并且不会被 IDE 当作非空类型进行推导。如果需要推导为非空,建议使用javax.annotation.Nonnull。
- Lombok 的
@Getter/@Setter: 生成字段的 get 和 set 方法。@ToString: 生成 toString() 方法。@EqualsAndHashCode: 生成 hashCode() 和 equals() 方法。@Data: 相当于同时使用了 @Getter、@Setter、@ToString、@EqualsAndHashCode 和 @RequiredArgsConstructor。@Slf4j:生成日志对象,可以在代码中用log.info啥的。@RequiredArgsConstructor:为所有 final 字段或 @NonNull 字段生成一个 Constructor。@NoArgsConstructor:生成一个什么也不做的 Constructor。不允许在有 final 字段的类里使用;如果要使用需要用@NoArgsConstructor(force = true),可以将其初始化为 default 值。@AllArgsConstructor:生成一个包含类中所有字段作为参数的 Constructor。字面意思。@Builder:builder 模式,不多说。@SuperBuilder(toBuilder = true):如果父类实现了@SuperBuilder,则子类也可以用 builder。注意不能和@Builder混用。
注解中再包含其他注解需要使用 @__ 语法。一个最常用的例子是 @RequiredArgsConstructor(onConstructor = @__({@Autowired})),在生成 Constructor 的同时将其标为 @Autowired。
Slf4j
可以在 @Slf4j 的 class 内部使用 log.xxx 进行日志打印(xxx=info, error, ...)。
- 首个参数可以是一个 fmt string,之后可以接 args。
- args 可以比 fmt 里的坑多一个 Throwable,这样打印的日志后面可以接这个 Throwable 的调用栈。这是很常见的用法。
Spring
后端必备套件之一。Spring 的核心是自动管理单例,因为在后端开发场景中单例实在是太多了,用 Spring 可以省去自己创建和取对象的麻烦。
- IoC(Inversion of Control)概念:Spring 启动时其会创建一个巨大的全局对象池,称为容器;并且其为每个 @Service 类在容器里创建一个单例对象。使用时只需通过 @Autowired 即可从对象池里拿到这个实例。
- 在 Spring 语境下,“Bean” 特指被 Spring 容器管理的对象。
Spring 在运行时会自动起一个 tomcat 来运行服务。(tomcat 是一种网络容器,用它可以方便地部署网络应用)
uri 相关注解
uri 一般是 /{context-path}/{Class-Mapping}/{Method-Mapping} 形式。
- context-path:一个全局前缀,用的比较少。直接搜
context-path即可。 - Class-Mapping:class 上定义的
@RequestMapping注解。 - Method-Mapping:类的方法的注解,可以是
@GetMapping等。 @RequestParam:指定 GET 请求的 http param。默认是必填 param 的,如果想要选填需要加required = false。
Swagger
用于自动生成 RESTful API 文档的工具。只需要注解即可生成。
Swagger 有 v2 和 v3 两个版本,看 import 能看出项目使用的是哪个版本。v3 版本在 import 语句里一定包含 v3. 字样。
v2
随便记点。
- 注意:swagger 生成的类型并不完全正确!!因为生成时依赖 Bean Getter,但是序列化和反序列化时又是依赖 Jackson 或者其他逻辑。
- 因此最好不要在 DTO 上使用
org.apache.commons.lang3.tuple.Pair,因为 swagger 不能正确生成它的类型。
- 因此最好不要在 DTO 上使用
- 对于
@XXX("xxx"),一般等价于@XXX(value = "xxx")。如果括号内有多个 kv,必须显式写value =。 @ApiModel:将 class 或 enum 设为请求参数 / 响应类型。@ApiModelProperty:修饰 ApiModel 的成员。- swagger v2 对 interface 的支持不太好,例如两个 class 实现了同一个接口,然后对这个接口做反序列化,swagger 不会自动关联到这两个类。有点致命。
Mybatis
后端必备套件之一。
Mybatis 是一个数据库操作套件,用于把数据库操作集成到代码中。我不认为其是一个 ORM,毕竟也需要自己写 SQL。
Mybatis 有两种模式,注解模式和 XML Mapper 模式。注解模式是在注解中写 SQL 语句,Mapper 模式相当于是一个 DSL。对于复杂查询或者大型项目,用 Mapper 模式的更多。
- 如果不想学 Mapper 模式的 DSL,可以让 AI 写或者翻译 SQL 到 Mapper。
- Mapper 模式下有
<mapper namespace="com.xxx.xxx">,这里的 namespace 必须是一个 xxx.java 模块的完整路径;在这个模块里写一个public interface XXXMapper,然后声明每一个接口方法即可。XML 标签的 id 必须与 java 接口的方法名一致。
JVM 调优
GC
- java 8 有这些 GC:Serial GC, Parallel GC, CMS (Concurrent Mark Sweep), G1 (Garbage-First)
- 后二者将 GC 分为几个阶段,只在其中部分阶段 stop the world
- CMS(标记 - 清除,没有整理):初始标记(停顿)-> 并发标记 -> 重新标记(停顿) -> 并发清除
- G1(标记 - 整理):类似文件系统分块,新生代和老年代不再连续。优点:可预测,无内存碎片
- ZGC:全部都是 Full GC;动态分块
- 使用 Load Barriers,进行类似 CAS 的复制块访问
- 染色指针,有四位信息存在指针本身上
- 优点:吞吐量大,停顿短,不需要太多干预;缺点:CPU 占用高
- java 的 gc 是从根节点开始扫描不可达对象。
- JDK8 的 GC 是分两代,新生代和老年代。新生代里又有三个区(eden, Survivor(s1, s2))。在新生代 gc 叫 minor gc,gc 老年代 + 新生代叫 full gc。
- 每次 gc 会使新生代对象的 age + 1,当 age 超过一定值时将被移入老年代
- Survivor 区的对象总年龄超过 50% 时也会将部分年龄最大的移入老年代
- java 8 的几个 GC 都是 stop the world 的,调优目的就是减少 full gc,减少 stop the world。
工具
jvisualvm 是 jdk 里自带的 GUI 分析工具,可以追踪 GC 状况,日志,系统资源占用等。
