Leon Ding Blog - java
type: channel
link: https://blog.leonding.me/tag/java/
items: Go 语言设计丑陋之处
items: Java 原生化的未来 Graal AOT 编译器
items: 使用 WSL + VS Code 现代 Java 开发
items: 函数式编程的思想和优势
items: 编码从来不是一件简单事情
items: Java 虚拟机 JVM 内部设计
items: Go 语言中的鸭子类型

Go 语言设计丑陋之处
type: item
date: 2024-11-03T02:24:00+08:00
description: 在互联网上经常可以看到程序员因为编程语言的选择而争论不休，常常出现类似 “XXX vs XXXX” 的评测文章，同时也不排除一些语言布道师或商业公司在背后炒作某种语言的热度。大多数文章的评测重点通常是程序的执行效率和内存占用，很少从语言特性、语法或语义的角度进行深入分析和讲解，我这篇文章将从这个角度来讨论 Go 语言设计丑陋之处。在我编程生涯中也就使用过很多门编程语言，这其中就包括所谓的大道之间的 Go 语言，总体来说使用 Go 语言来编写逻辑是非常容易的，天生支持协程，采用 CSP 和 GMP 模型的高并发的语言，使得开发高并发应用也非常简单。它的语法简单和 25 关键字非常容易掌握，能使程序员快速掌握并且编写代码逻辑，并不意味着 Go 的设计有多么好。反而在我使用 Go 编程过程中出现很多感觉很丑陋的设计，而不像国内某些 Go 语言布道师吹崇它多么多么好，我个人感觉哪些很多布道师从 PHP 转换过来的，就没有写过其他高级的编程语言，例如 Kotlin 和 C# 、Rust 或者新版本的 Java 等... 如果读者是 Go 语言的粉丝，这篇文章的内容会让你感到非常不适，乃至会让你感到作呕。错误处理Go 程序的错误处理，在编写逻辑复杂并且存在多种复杂异常的情况时会出现写出满屏 if err != nil，这个和 C 语言的一脉相承的，C 语言使用是错误码，Go 的错误处理只是添加一些上下文描述，并没有附加堆栈信息，例如下面的代码： if len(files) <= 0 { // 它这个错误设计要不停写 if err != nil err := lfs.createActiveRegion() if err != nil { return err } return nil }但一般程序员会自己优化掉，写成下面的这种方式：// return lfs.createActiveRegion()可以直接这么写，但是会丢弃错误处理上下文，特别是在处理一些比较细节的错误分类的时候。Go 的错误处理机制中，错误值通过返回值 return 在调用栈中逐层向上冒泡传递，每个函数在检测到错误时，通常会将错误返回给上层调用者，由上层决定如何处理。这对于喜欢函数编程的程序员是一个很糟糕的设计，例如下面代码： sql2 := gsql.SelectAs([]string{"name", gsql.As("age", "年龄"), "id"}).From(UserInfo{}).ById(2) // SELECT name, age AS '年龄' FROM UserInfo WHERE id = 2 t.Log(sql2) sql3 := gsql.SelectAs(gsql.Alias(UserInfo{}, map[string]string{ "name": "名字", })).From(UserInfo{}).ById(1) // SELECT id, name AS '名字', age FROM UserInfo WHERE id = 1 t.Log(sql3)这段代码核心功能是我编写的一个用于 Go 的结构化查询语言代码生成器，类似于一个 SQL 在 GO 语言中 DSL 抽象实现，可以生成出 SQL 查询语句的字符串。但在调用这个 gsql 包提供的各种函数和 API 过程中会出现一个问题就是每个函数逻辑复杂有其他逻辑分支，可能会出现 error 的情况，因此按照 Go 的错误处理设计，每个函数必须返回其函数内部的 error 作为上下文，传递出去方便在出错时定位到问题。[scode type="yellow"]但为了满足写起来像函数编程链式调用的风格，又不得将这些函数的错误信息隐藏，包装起来，只有当调用完最后一个函数才能获取到具体的错误信息。每个函数在处理之前都需要检查基于上一个函数调用的结果的有没有出错，如果出错了就直接 return err ... 白白的创建一个并且运行新的栈帧...[/scode]return 和 defer 冲突在多数编程语言 return 作用都是用来结束栈帧，结束函数的执行，并且如果函数签名中有返回值，那么还要顺带着返回值一起返回，但在 Go 语言还设计了 defer 关键字来辅助 return 来完成一些辅助清理工作，defer 关键字是用来延迟执行某个函数用途，并且保证在函数执行栈出现异常或者 return 时，能正常执行某个预定函数操作。在结合 defer 场景下就会出现问题，为了防止文件句柄打开忘记关闭情况下，在打开文件操作的代码逻辑后面就会紧跟上 defer 的操作，但 defer 并不能处理这个函数的调用返回值，return 和 defer 关键字的在关闭文件描述时发生干扰，例如下面的代码： f…

Java 原生化的未来 Graal AOT 编译器
type: item
date: 2024-07-17T22:35:00+08:00
description: Java 这门语言与生俱来的显著特性就是“一次编译，到处运行”，这种功能得益于 JVM 平台的支持，Java 程序通常通过将其打包为 JAR 或 WAR 包，并依赖 JVM 和 Servlet 容器来运行。其底层运行时 JVM 采用 JIT（即时编译）模式来执行程序代码，JVM 会在运行时进行编译优化和动态执行代码，这通常会导致较高的内存占用。这样的好处是采用 JIT 可以热更新和热部署程序，并且 JVM 可以在运行期间对程序进行动态分析，来实时优化程序以达到最好的性能状态。Cloud Native 面临的问题随着云计算的发展，服务端程序的运行和部署方式发生了巨大变化，特别是在云原生（Cloud Native）场景下，服务器程序被打包成一个个容器运行。在这种环境下，传统方式容器化的 Java 程序面临着打包后的镜像文件过大的问题。镜像文件中需要包含 JRE 和各种第三方库，导致镜像体积庞大，占用更多的磁盘空间。目前云计算的计费方式也发生了改变，采用按需分配的实时计费方式，根据程序的内存占用和运行时间计费。这与传统 IDC 自托管式机房中长时间运行服务器程序形成鲜明对比，按需分配的计费方式显然更具性价比，因此优化 Java 程序在容器环境中的运行方式变得尤为重要。Serverless 面临的问题传统的 JVM 工作原理要求程序员先将编写的 Java 程序打包为 JAR 或 WAR 包，这是普通 Java 开发者所能接触到的程序编译和构建阶段。然而当程序运行起来之后，底层的 JVM 会进行许多复杂的操作，包括内存管理、垃圾回收、即时编译等。需要先启动 JRE 中的 JVM 来加载 Tomcat 服务器所需的字节码，再加载 WAR 包中的字节码文件到 Servlet 容器中运行服务，这个过程非常耗时。下图为整个传统 Java 程序从源代码到程序运行各个阶段的工作流程：下图为传统的 Java 运行时执行程序的生命周期，首先应用的开发者将 *.java 源代码编译为 bytecode 和 jar 依赖包通过 maven 构建打包为一个单独的 jar 包。当执行 java -jar xxx.jar 命令时，首先要启动 JRE 中的 JVM 程序，因为 JVM 本身也是一款单独程序对应着 $JAVA_HOME\bin 目录中的 java ，当 JVM 启动之后会在系统中创建一个进程，将要运行的 jar 程序包和 JRE 标准库中 class 文件加载入 JVM 内存中，并且找到 main 方法开始解释执行。当程序运行一段时间之后 JVM 充分获取了 Java 程序的一些执行数据信息，开始对程序进行 JIT 及时编译优化程序，当程序被 JIT 优化之后的程序才能达到和 C/C++ 这种直接编译为 Native Code 程序的性能。在云计算推崇的 Serverless 架构场景下，Java 的 JIT 模式并不适合，Serverless 应用程序需要能够快速启动以响应事件驱动的请求，并且在空闲时自动关机销毁，释放在服务请求期间所使用的内存。然而，这种需求与传统的 Java 程序运行模式背道而驰，使得 Java 在 Serverless 场景中显得不适用，这就是被业界称为的 冷启动 问题。为了解决这一痛点，Oracle Labs 推出一款新的 JDK 产品 GraalVM JDK ，相比传统的 JDK 该产品内置 Java 静态编译工具和一款新的虚拟机，和传统的 JVM 不一样 GraalVM 是支持多语言进行混合编程的虚拟机，这也是它一大亮点。GraalVM 中的静态编译工具不仅是支持将 Java 编译为二进制可执行文件，还支持 JavaScript 和 Python 等其他语言的编译优化功能，本文只关注于 GraalVM 对 Java 程序进行 AOT（Ahead-Of-Time）编译的解决方案。使用 GraalVM 对 Java 程序进行 AOT 编译的解决方案，可以显著缩短启动时间并降低内存占用，使 Java 程序更适合在 Serverless 环境中运行，通过 AOT 编译，编译器生成一个 Native Code 二进制文件，即对应平台架构的机器代码程序。这种方式无需像 JIT 那样依赖于 JVM 运行，在内存和磁盘占用上有显著改善。下图为 AOT 编译工作流程，在编译阶段会程序进行静态分析，通过它内置工具分析 Java 源代码中依赖关系，将其所有依赖和代码执行逻辑都进行提前编译为机器代码，缺点也很明显可能会失去在 JIT 模式下的动态反射功能。如上图，在 AOT 模式下将在运行时的过程放到在程序构建阶段，构建阶段会对 Java 程序执行静态代码分析和依赖可达性分析，将其所有的依赖软件包都编译生产所属平台对应的可执行二进制文件。AOT…

使用 WSL + VS Code 现代 Java 开发
type: item
date: 2024-06-17T16:55:00+08:00
description: 在我的 Java 编程生涯中使用过很多 Java 开发环境，从编写第一个 Java 程序使用的 Notepad++ 到 Eclipse 再到 IntelliJ IDEA。能编写 Java 的 IDE 产品有很多，如何选择一个好的 Java 开发环境？这对于从事 Java 程序设计的程序员来说是一个重要的问题。选择一个合适的 IDE 不仅能提高开发效率，还能减少调试和排错的时间。本篇文章将分享一些选择 Java 开发环境的关键考虑因素，并介绍如何通过 WSL + VSCode 打造一个混合开发基础 IDE 环境来开发 Java 程序。从事 Java 程序开发的程序员主流都是做服务器端程序开发。在编写程序项目源代码阶段，通常使用 Windows 系统的 PC 进行开发工作，而当程序部署上线之后，往往运行在 Linux 系统环境中。这就要求我们不仅要熟悉 Windows 系统，还要熟悉 Linux 系统，才能更好地编写和调试 Java 程序。这也是选择开发环境时需要考虑的一个重要因素，因为一个好的 IDE 应该能够方便地在不同操作系统之间切换，并提供良好的跨平台兼容性。目前主流的 Eclipse 和 IntelliJ IDEA 都是重量级的 Java 集成开发环境，并且针对不同系统架构开发了对应的版本。程序员在 IntelliJ IDEA for Windows 版本下开发，就必须使用 Windows 下的一些开发和调试技巧，而在 IntelliJ IDEA for Linux 中又需要掌握一些 Linux 下特有的功能和设置。那么有没有一种折中的解决方案，可以在 Windows 开发 Java 程序并且能获得 Linux 系统中的一些开发体验和特有功能呢？答案是有的。[scode type="green"]使用 Microsoft 推出的轻量级 IDE 产品 VSCode 和最新版本 Windows 系统所提供的 WSL（Windows Subsystem for Linux）功能就可以实现这种开发模式。我个人认为此种开发方式是更符合现在 Java 程序开发的方式，虽然 JVM 为 Java 程序提供强大跨平台能力，能让 Java 程序一次编写到处运行，但在开发阶段会不可避免的使用所在的开发机上特有命令和功能。对于需要跨平台开发和调试的需求，使用 VSCode 与 WSL 的组合可以在 Windows 环境中提供类似 Linux 的开发体验，能在够让 Windows 和 Linux 系统之间文件和命令互相操作，这种方式不仅简化了环境切换还能提高开发效率，目前最新版本的 IntelliJ IDEA 的 UI/UX 设计越来越像 VSCode 了，所以个人还是更推荐 VSCode 作为基础的 IDE 产品。[/scode]基础 WSL 配置使用 WSL 和 VSCode 作为基础开发环境，最低要求操作系统为 Windows 10。如果使用 Windows 11 则更为理想。在 Windows 11 系统中，WSL 功能已默认开启，但要正常使用还是需要一点额外的操作。WSL 本身不提供完整的 Linux 系统功能，只是提供了一个与 Linux 兼容的内核接口层和虚拟化技术在轻量级虚拟机 VM 中运行 Linux 内核，再配合其兼容层的功能使用户能够无缝结合使用 Windows 和 Linux 的功能。[scode type="yellow"]默认情况下，WSL 并未内置 Linux 系统镜像，需要开发者自行选择和安装相应的 Linux 发行版。[/scode]可以通过在终端中输入以下命令来查看 WSL 官方提供的 Linux 发行版：# 查看能安装的 WSL Linux 版本 wsl --list --online通过列出的版本信息，建议使用默认的 Ubuntu 即可，按照指定的 Linux 发行版本到 Windows 11 系统中：# 指定对应的 Linux 版本进行安装 wsl --install -d Ubuntu通过以上命令就可以安装 Linux 发行版本到 Windows 系统中，命令执行流程如下图：当有多 Linux 版本环境需求时，可以进行多个版本的选择性安装，同样使用 wsl --install -d <发行版本名称> 来获取相应的版本安装，查看已经安装的 Linux 发行版本可以使用 wsl --list --verbose 命令查看，下面是一组常用的命令：功能命令安装指定的版本wsl --install -d <发行版本名称>查看已经安装的版本wsl --list --verbose选择启动指定的 Linux 发行版wsl -d Ubuntu更新 WSL 程序wsl --update查看 WSL 状态wsl --st…

函数式编程的思想和优势
type: item
date: 2024-04-15T01:37:00+08:00
description: 自从高中开始学编程已经使用过好几门（PL）编程语言了，目前主流最多使用的编程语言 C/C++ 、Java 、JavaScript 这些都属于结构式和命令式编程语言。但在 Java 在 8 版本发布之后引入了 Lambda 表达式方式对函数式编程的支持，从现代的 Java 设计角度上来说并不是一个纯粹的结构和命令式编程语言了，个人将其归属为一个综合性很强的多编程范式的语言。在过去一年的重新学习了基于 ES6 版本的 JavaScript 之后，让我对函数式编程语言更为感兴趣了。在此之前使用过 Go 语言的实现过一些类似于函数式编程的代码，但 Go 在语言设计的时候并没有添加三目运算符和 Lambda 表达式，使其在编程体验上逊色于 JavaScript 和 Java8 这类原生支持三目运算符和 Lambda 表达式的函数式语言，目前我已经从完全放弃了 Go 语言传统命令式进行编程了，除了几个极少历史遗留开源项目正在使用它进行维护之外。如果读者正在考虑深入学习一门 PL 编程语言的话，我建议学习并且使用现代 Java21 之后的版本，现代 Java 已经添加很多新特性，这篇文章我会介绍一下函数式编程的优势。面向过程程序员日常开发中使用的最多编程方式是面向过程和面向对象，面向对象 OOP 是从 C 语言的面向过程演变发展设计出来的；面向过程取决于程序员如何设计一个好的数据结构和用编写好的函数去操作数据完成逻辑计算，在面向过程编程代码逻辑过程中，函数是主要的组织单元，程序的执行是一系列的函数调用，使用结构体来组织数据，但是能操作这些结构体仍然是通过函数来进行的。[scode type="yellow"]编程范式最典型的例子，生活中一件事情抽象到计算机程序如何做？例如一个抽象的问题：如何把一头大象放到冰箱里面该如何做？换到人类的思维第一反应是怎么可能会有冰箱能装得下一头大象？在计算机里你只要想将具体逻辑告诉它，它能可以按照你逻辑去做某件事情，它很笨不会考虑这么多其他因素，不能去考虑大象是不是比冰箱大，除非在装填函数中编写了一些逻辑来检查是否合规。[/scode]下面就是使用 C 语言就编写一个 3 个函数把大象结构放到冰箱结构里，每个步骤对应着一个操作函数，面向过程的窜行代码：#include <stdio.h> #include <stdbool.h> // 定义大象的结构体 struct Elephant { char name[20]; int weight; }; // 定义冰箱的结构体 struct Refrigerator { bool isOpen; bool hasElephant; }; // 函数原型 void openRefrigerator(struct Refrigerator *fridge); void closeRefrigerator(struct Refrigerator *fridge); void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge); int main() { // 创建大象对象 struct Elephant elephant = {"大象", 5000}; // 创建冰箱对象 struct Refrigerator refrigerator = {false, false}; printf("将大象放到冰箱里面：\n"); // 打开冰箱 openRefrigerator(&refrigerator); // 将大象放到冰箱里 placeElephantInRefrigerator(&elephant, &refrigerator); // 关上冰箱 closeRefrigerator(&refrigerator); return 0; } // 打开冰箱的函数 void openRefrigerator(struct Refrigerator *fridge) { printf("打开冰箱门\n"); fridge->isOpen = true; } // 关闭冰箱的函数 void closeRefrigerator(struct Refrigerator *fridge) { printf("关闭冰箱门\n"); fridge->isOpen = false; } // 将大象放到冰箱里的函数 void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge) { if (fridge->isOpen) { printf("将 %s 放进冰箱\n", …

编码从来不是一件简单事情
type: item
date: 2024-03-22T18:32:00+08:00
description: 对于写代码和编写程序来说，放到今天稍微学习一下 Python 和 JavaScript 这类编程语言就可以实现满足自己需求的应用程序，因为已经有很多很好封装好的框架共我们开发者使用，乃至一个中学生都不需要懂一些数学知识，调用相关的 Math 包就可以进行高等数学相关计算。现在有许多高级的编程语言和优秀的框架，使得开发变得更加容易和高效，Python 和 JavaScript 等语言拥有丰富的生态系统和强大的社区支持，为开发者提供了各种各样的工具和框架，使得构建复杂的应用程序变得更加容易。上文提到从需求变成编程再到程序，需求是代码编写者对于软件系统所需功能和特性的描述，例如实现一个四则运算计算器程序，可以是在程序直接编写 console.log("20 + 4 = ",20 + 4) 这种形式来完成需求，也可以是实现一个漂亮的界面完成对应的需求。从 IDEA 到 Coding 到 Product 这个过程中 coding 环节是属于技术实现环节，实现某个功能可以有很多种技术方案，但是方案这么多作为程序员如何选？主流开发的软件都属于应用软件，目前跑在电脑上和跑在手机上的应用软件并没有一些本质区别，都是程序员将自己的大脑逻辑抽象通过代码的形式告诉计算机，当执行这段代码运行之后该程序就会按照你编写的逻辑去交给 CPU 执行，只不过 CPU 要比我们脑子里面计算快得多。这个过程就是 Coding 阶段不能确保程序员大脑里的逻辑一定是正确的，当不合规的逻辑被编写到程序代码中，就可能会产生很严重的后果，例如程序崩溃导致的事件。在电脑和手机只要你开发的不是一些基础的软件，应用软件遇到问题最多就是卡死重启一下，现在的服务器端软件更是搞出来主从或者高可用架构来解决程序员在编码阶段留下的 bug 的问题。在工业时代崇尚机器自动化，现在计算机程序已经渗透到各行各业中了，天上飞的地上跑的水下游的，飞机汽车潜艇这些设备上都靠着计算机程序在辅助人类完成一些操作。机械工程师在设计一些机械结构时犯下的错可能会导致飞机会坠毁，即使是设计合理的机械结构现在控制这些机械装置的已经不是人类了，是跑在计算机中的程序在控制这些机械结构。所以一架飞机或者自动化驾驶的汽车出了事故，责任不完全在机械工程师身上，更有可能的是编写软件的工程师导致的。即现在从一名普通人转行为一名程序员太容易了，普通人通过特定的 IT 培训经过几个月的训练就可以写代码，此种工程师可能不具备专业的 CS 知识就可以动手写程序了，带了问题就上面所说的不可靠软件，如果需要开发专业型的软件还是需要专业型的知识作为铺垫的，例如让一个学土木工程或者做建筑设计的去做软件工程师，那就对应上了中文的俗语 “隔行如隔山” 。一些知名计算机科学界的先驱们就提出了点观点 Dijkstra 曾说过：[scode type="yellow"]我们所使用的工具对我们的思维习惯有着深远的影响，因此也对我们的思维能力有着深远的影响。......使用 COBOL 会削弱人的思维；因此，教授 COBOL 应被视为刑事犯罪。[/scode]简单的工具容易被使用，但是会让使用者不去思考背后的原理，导致缺乏逻辑思维和不去思考事物的本质。这在日常生活中使用的智能手机可以拍照可以打电话可以玩游戏，但是发明和制造一台手机所需要的知识远远不是一个人就能做到的，是一个跨学科和跨领域合作的产品。现在用电脑写一篇文章的时候打错字可以回退修改，但是如果在纸上你写错了一个字可能就报废一张纸，解决办法则你可以使用铅笔进行写字配合着橡皮擦使用，古代人使用毛笔更需要思维和手法练习才发展出来了书法行业。现在已经很少有人去使用了 COBOL 语言进行程序设计了，虽然我本人也是没有写过，但是在研究一些 PL 过程中了解到 COBOL 这门 PL 语言属于结构化加命令式结合产物。但是新的流行语言例如脚本语言和弱语言 JavaScript 和 Python 这些只有在运行的时候才能确定某个变量的类型，还有 C/C++ 这类有空指针设计的 PL ，也会带来底层软件的 Bug ：使用这里语言开发的程序如果程序在开发期没有有效找出 bug 或者程序员的逻辑错误导致 bug 产生，当程序发布之后这些带有 bug 程序会被发布到线上环境，可能在未来的某一时刻会因为这些 bug 导致程序出现故障从而影响到整个系统。一些隐秘的 bug 不一定尽快被修复，例如一个计算闰年和数据类型存储精度丢失的 bug ，要导致触发这个 bug 条件是当前是时间是处于闰年或者闰秒才会发生错误。这类隐秘的 bug 是开发阶段程序员逻辑错误导致的没有处理好，在修复的这些 bug 过程中需要花大量的时间解决，如果在开发这些涉及相关领域知识软件系统时应该提前去查看一些相关领域的资料文件，使用一些业绩主流的解决方案，例…

Java 虚拟机 JVM 内部设计
type: item
date: 2023-03-09T23:06:00+08:00
description: 大多数开发者或多或少都使用过 Java 这门编程语言，Java 这门编程语言最大的特性就是一次编写到处运行，这就使得开发者在编写了 Java 源代码之后通过 Java Compiler 编译出来的程序就可以在不同平台架构上运行。不管是 x86 还是 ARM 的 CPU 都运行正常无误，熟悉计算机 CPU 架构都知道不同 CPU 核心使用的不同架构和指令集，如果想要编写一次程序到处运行那就得需要使用交叉编译来完成，但是 Java 打破这一点，这得益于 JVM 的存在，Java 源代码不是将源代码直接编译成为对应平台机器码，而是自己定义一套指令集规则也就是对应的 Java Bytecode 标准，再通过将其字节码文件放到 JVM 上运行，最终 JVM 充当一个翻译员将其通过 JIT 的方式运行程序。[scode type="green"]JVM 有自己完善的硬体架构，如处理器、堆栈、寄存器等，还具有相应的指令系统。JVM 屏蔽了与具体操作系统平台相关的信息；本质上就是对现有的物理硬件的软件抽象的实现，可以作为一种编程语言的虚拟机，实际上不只是专用于 Java 语言，只要生成的编译文件符合 JVM 对加载编译文件格式要求，任何语言都可以由 JVM 编译运行。目前实现这些标准虚拟机有很多，例如 HotSpot 、 JRockit 、 IBM OpenJ9 、 GraalVM 这些都是相关的虚拟机实现产品。[/scode]Java 跨平台原理编写 Java 程序的源文件都是以 .java 为后缀名的文件，下面则为一个 Java 源代码文件要想运行它必须通过 Java 编译器来生成对应的字节码 .class 文件，从而让不同平台的 JVM 加载 .class 文件来运行程序：这里值得注意的是 Java 现在每 6 个月就有一次更新，每次都会有新的语言特性添加，不同版本的 Java 编译器可能生成的代码文件在不同 JVM 有不同效果，例如低版本的 JVM 不能运行高版本的 Java 编译器生成的 .class 文件，反过来则是可以的，不过这些都是小问题可以通过 AOT 编译成对应平台机器码运行，也可以使用一些第三方提供程序版本代码迁移工具 EMT4J 来完成。在 JDK 整个开发工具包中其实有两套编译器，大多数程序员所使用的 javac 编译器，此编译器只是将编写的 java 源文件编译生成为 JVM 能认识的字节码；而在 JVM 中又有一套 JIT 编译器，针对的是更高性能程序场景使用的，会把字节码再次编译成为机器码。JVM 主体架构当 Java 程序需要运行的时，此时就得需要 JVM 了，JVM 内部大致会和下图接近，首先前面通过 Java Compiler 生成的 .class 文件会把加载到虚拟机内存中，首先头一次执行是通过 JVM 内置 Interpreter 解析执行代码，这个过程 JVM 的解析器会把字节码里面指令翻译成对应平台的指令进行运行。另外 JVM 中内置另外 2 个 JIT Compiler 分别为 C1 和 C2，这两个即时编译器是针对长时间运行的热点代码进行优化的编译器，JVM 的设计者在设计的就考虑到一些程序代码运行效率的问题，前面提到的通过解析器执行代码性能肯定不能和 C 这种原生编译成机器码的程序性能相比，所以设计者针对热点代码会采用 C1 和 C2 编译器进行编译较为底层次的机器码来执行。[scode type="yellow"]使用解释器执行时，当某一行代码被执行了几次，这行代码会被打上 warm 的标签，当某一行代码被执行了很多次，这行代码会被打上 Hot🔥 的标签成为热点代码，这和 JVM GC 分代设计方式类似，通过分代来解决不频繁 GC 放到老年代。[/scode]在运行时将热点函数编译为汇编代码，当程序再次运行到经过实时编译的函数时，就可以执行经过编译和优化的汇编代码，而不再需要解释执行了。由于编译是在运行时进行的，因此 JIT 编译器可以获得代码实际运行的路径、热点和变量值等信息，基于此可以做出非常激进的编译优化，从而获得执行效率更高的代码。何为热点代码？例如下面有一段代码就为热点代码，当然现实中程序热点代码可能更为复杂：public class HotSpotExample { public static void main(String[] args) { long startTime = System.currentTimeMillis(); int iterations = 100000000; for (int i = 0; i < iterations; i++) { // 热点代码块 int result = compute(i); if (result % 2 == 0) {…

Go 语言中的鸭子类型
type: item
date: 2023-03-08T03:11:00+08:00
description: 在程序设计中有一种编程风格叫 Duck Typing ，这种编程风格实际上是面向对象编程语言中所提倡的多态的表现，通过将某一类对象的行为抽象成为一个集合，当前对象的类型由这些抽象层所决定的，这和前面一篇我博文中提到抽象概念类似。典型的应用就是 Go 语言也提供了接口类型，使得我们可以面向接口编程，将实现和接口分离。在我看来，软件的抽象之美也应该以此来表达，和 Java 语言不同的是 Go 并不是那么 强制 ，它使用了一种鸭子类型的方式让动态类型成为可能。目前 Go 只是某方面代码写的少有着较明显的特性，但是如果一个超大型项目未必有 Java 做的好，另外 Java 最新的设计草案 Implicit Classes and Enhanced Main Methods (Preview) 开始考虑把一些关键字默认隐式的使用，并且帮助学生和教育工作者能快速的了解 PL 设计。Duck Typing在 Go 中没有 implements 和 extends 这种关键字，这对我们而言反倒轻松了一些，它认为 Go 的接口就像鸭子测试里的描述：当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子，那么这只鸟就可以被称为鸭子。在使用鸭子类型编程语言中，更应该关注的是对象的行为，对能干什么？对象有什么样的能力，这和上篇文章中介绍的多态和抽象接口是类似的，如果一组对象有相似的行为功能可以抽象的划分成为一类。在通常编程过程中我们编写一个函数，函数接受某个类型的对象，并在函数内部调用它的行为，那这个函数只能接受某个特定类型的对象。而在提倡使用鸭子类型的语言中，这个函数可能就被修改成接受某个行为集合的对象作为参数，只要某个类型对象具备这个行为功能，那么就能被这个函数准确调用和运行，反之则会出现运行时错误。抽象鸭子我们用 Go 语言来实现一个鸭子🦆类型：type Duck interface { Swim() // 游泳 Feathers() // 羽毛 }这里使用 Go 提供的 interface 关键字定义了一个鸭子接口类型，这个接口中提供了鸭子的两种行为：游泳和羽毛是什么样的，但是没有提供实现。我们见过的鸭子类型可多了去了，下面是你可能见过的：组合接口现在我们给鸭子再添加一种嘎嘎叫的能力，一种方式是在原有的接口上添加 “嘎嘎叫” 方法，这样做的话就表示所有的鸭子都应该拥有此能力，假设我们的玩具鸭并不能开口嘎嘎叫，所以它没有这种能力。这时候我们可以将会嘎嘎叫的鸭子单独定义一种类型，在 Go 可以使用组合的方式来实现：type QuackDuck interface { Quack() // 嘎嘎叫 Duck // 嵌入接口 }这样 QuackDuck 类型就拥有了之前 Duck 提供的两种抽象能力，同时还应该拥有嘎嘎叫的能力。接口实现前面我们只给出了鸭子的能力定义，还没有任何实现，由于 Go 中没有继承和实现的关键字，想成为上述接口的实现非常简单，只要实现它们定义的方法就可以了。// RealDuck - 真正的鸭子 type RealDuck struct { } func (RealDuck) Swim() { fmt.Println("用鸭璞向后划水") } func (RealDuck) Feathers() { fmt.Println("遇到水也不会湿的羽毛") } func (RealDuck) Quack() { fmt.Println("嘎~ 嘎~ 嘎~") } // ToyDuck - 玩具鸭 type ToyDuck struct { } func (ToyDuck) Swim() { fmt.Println("以固定的速度向前移动") } func (ToyDuck) Feathers() { fmt.Println("白色的固定的塑料羽毛") }可以看到我们定义了两种鸭子类型，一种是真正的鸭子，它还多实现了一种嘎嘎叫方法，另一个玩具鸭子只有游泳和羽毛这两种行为。这个编程方式和我们写普通的结构体方法没什么区别，只是对应的方法签名相同，其实这种方式在 Go 语言的标准库中有特别多的应用，比如：io.Reader、io.Writer 和 io.Closer。接口使用接下来我们可以使用一下这个类型了：var duck Duck duck = ToyDuck{} duck.Swim() duck.Feathers()输出以固定的速度向前移动 白色的固定的塑料羽毛 由于玩具鸭没有嘎嘎叫的能力，所以如果你这么写编译无法通过错误实现，我们也可以用一种工厂的方式来进行调用：func Factory(name string) Duck { switch name { case "toy": return &ToyDuck{} case "re…