跳到主要内容

Java泛型中的默认类型及基本类型的自动装箱和拆箱机制

· 阅读需 1 分钟
素明诚
Full stack development

泛型机制​

  1. 如果没有进行具体类型的传入,默认使用 Object 进行装箱。
  2. 取值的时候自动拆箱为基本类型。

举例​

package com.Generics;

public class Box<T> {
private T value;

public Box () {
}

public Box (T value) {
this.value = value;
}

public T getValue () {
return value;
}

public void setValue (T value) {
this.value = value;
}
}

使用​

package com.Generics;

public class BoxTest {
public static void main (String[] args) {
Box genericBox = new Box(); // 不指定泛型类型,默认为Object
genericBox.setValue("Hello"); // 可以存储任何类型的对象
genericBox.setValue(123); // 也可以存储一个int

Object genericValue = genericBox.getValue(); // 返回值类型为Object
System.out.println(genericValue);

Box<Integer> intBox = new Box<>();
intBox.setValue(42); // 自动装箱:将int转换为Integer

int intValue = intBox.getValue(); // 自动拆箱:将Integer转换为int
System.out.println(intValue);

}
}

从Object genericValue = genericBox.getValue()这段代码没有报错就可以判断出默认类型为Object

Maven 插件

· 阅读需 3 分钟
素明诚
Full stack development

什么是 Maven 插件?​

Maven 插件是一组预打包的 Maven 扩展,它们提供了用于构建和管理项目的功能。每个插件都由一个或多个目标组成,这些目标代表了可执行的任务。

常见的 Maven 插件​

maven-compiler-plugin​

  • 用途: 编译 Java 源代码。
  • 绑定生命周期阶段包括:compile (编译主源代码) 和 testCompile (编译测试源代码)。

maven-surefire-plugin​

  • 用途: 执行单元测试。
  • 绑定生命周期阶段是 test,用于运行单元测试并生成报告。

maven-jar-plugin​

  • 用途: 构建 JAR 文件。
  • 绑定生命周期阶段是 jar,用于将编译后的代码打包为 JAR 文件。

maven-war-plugin​

  • 用途: 构建 WAR 文件,通常用于 Java Web 应用程序。
  • 绑定生命周期阶段是 war,用于打包为 WAR 文件。

maven-install-plugin​

  • 用途: 将项目的构建输出(如 JAR 或 WAR 文件)安装到本地 Maven 仓库,以便其他项目使用。
  • 绑定生命周期阶段是 install。

maven-deploy-plugin​

  • 用途: 将构建输出部署到远程 Maven 仓库,通常用于与其他开发者共享或发布构建产物。
  • 绑定生命周期阶段是 deploy。

maven-clean-plugin​

  • 用途: 清理构建产物,如 target 目录。
  • 绑定生命周期阶段是 clean。

maven-site-plugin​

  • 用途: 生成项目文档和报告,如 Javadoc、代码覆盖率报告等。
  • 绑定生命周期阶段包括:site (生成站点文档) 和 site:deploy (部署站点文档到服务器)。

maven-failsafe-plugin​

  • 用途: 执行集成测试。
  • 绑定生命周期阶段包括:integration-test 和 verify。

插件的工作方式​

插件通常绑定到生命周期的某个阶段。当该阶段被执行时,与其绑定的插件目标也会被执行。例如,maven-compiler-plugin 的 compile 目标默认绑定到 compile 阶段,这意味着当 compile 阶段执行时,它也会执行。

自定义插件配置​

在 pom.xml 文件中,你可以为插件提供自定义配置。这允许你改变插件的默认行为。例如,你可以更改 Java 的编译版本,或为单元测试指定特定的参数。

创建自定义插件​

如果现有的插件不能满足你的需求,你可以创建自己的 Maven 插件。虽然这需要更深入的 Maven 和 Java 知识,但这为你提供了强大的自定义和扩展能力。

Maven 生命周期

· 阅读需 4 分钟
素明诚
Full stack development

Maven 的生命周期是一系列定义好的阶段,用于描述构建过程的不同步骤。当你执行一个 Maven 命令(如 mvn clean 或 mvn install),你实际上是在调用生命周期中的一个或多个阶段。

Maven 主要有三个标准的生命周期:

  1. clean:清除之前构建的项目。
  2. default(或 build):构建项目的主要生命周期。
  3. site:生成项目的站点文档。

默认生命周期 (主要用于构建和部署):​

  • validate: 验证项目是否正确,并且所有必要的信息都可用。
  • initialize: 初始化构建状态,例如设置属性或创建目录。
  • generate-sources: 生成任何需要编译的源代码。
  • process-sources: 处理源代码,例如过滤任何值。
  • generate-resources: 生成资源。
  • process-resources: 复制并发送资源到目标目录,为打包做准备。
  • compile: 编译项目的源代码。
  • process-classes: 对编译后的字节码进行操作,如字节码增强。
  • generate-test-sources: 生成测试代码。
  • process-test-sources: 处理测试代码。
  • generate-test-resources: 生成测试所需的资源。
  • process-test-resources: 复制并发送测试资源到目标目录。
  • test-compile: 编译测试源代码。
  • test: 使用合适的单元测试框架运行测试。
  • prepare-package: 在实际打包之前进行必要的操作。
  • package: 将编译后的代码打包为分发格式,如 JAR、WAR。
  • pre-integration-test: 在集成测试之前进行必要的环境设置。
  • integration-test: 处理和部署包到集成测试环境中,并运行。
  • post-integration-test: 在集成测试之后进行必要的操作。
  • verify: 检查包的有效性,并确保满足质量标准。
  • install: 安装打包的项目到本地仓库,供其他项目使用。
  • deploy: 在构建环境中,将最终的包复制到远程仓库,供共享和合作。

1. 清理生命周期:​

  • pre-clean: 执行清理前的需要的操作。
  • clean: 移除先前的构建产物。
  • post-clean: 执行清理后的需要的操作。

2. 站点生成生命周期:​

  • pre-site: 执行生成站点前的需要的操作。
  • site: 生成项目的站点文档。
  • post-site: 执行生成站点文档后的需要的操作。
  • site-deploy: 部署生成的站点文档到指定的服务器。

执行生命周期​

当你运行一个生命周期阶段时,Maven 会按顺序执行该阶段之前的所有阶段。例如,如果你执行 mvn install,Maven 会按顺序执行从 validate 到 install 的所有阶段。

生命周期和生命周期阶段​

生命周期 (Lifecycle):​

  • 生命周期是构建过程中的一系列阶段的集合。Maven 定义了三个主要的生命周期:default(主要用于构建项目)、clean(用于清理项目)和 site(用于生成项目的站点文档)。
  • 这些生命周期定义了一个项目从创建到部署的整个构建过程。

生命周期阶段 (Lifecycle Phase):​

  • 生命周期阶段是构建过程中的一个步骤或任务。例如,compile、test 和 install 都是生命周期阶段。
  • 当你执行一个生命周期阶段时,Maven 会按顺序执行该生命周期中直到该阶段为止的所有阶段。例如,执行 mvn install 时,Maven 会按顺序执行 default 生命周期中从 validate 到 install 的所有阶段。

你不能直接添加新的生命周期。Maven 的生命周期是预定义的,它包括 default(用于构建),clean(用于清理)和 site(用于生成项目文档)这三个主要的生命周期。

但是,你可以做的是:

  1. 自定义生命周期的阶段:虽然你不能添加新的生命周期,但你可以为已有的生命周期阶段定义自定义的行为。这是通过绑定插件的目标到特定的生命周期阶段来实现的。例如,你可以绑定一个插件的目标到 compile 阶段,使得在编译时执行特定的操作。
  2. 创建自定义插件和目标:如果 Maven 的内置插件和目标不能满足你的需求,你可以创建自己的插件,并定义新的目标。然后,你可以将这些自定义目标绑定到生命周期的特定阶段。
  3. 使用 Profile:Profile 允许你为不同的构建环境和条件定义不同的设置。虽然这不是添加新的生命周期,但它可以让你根据不同的情境改变构建的行为。

HTTP请求参数类型及其在Swagger文档和实际请求中的表示方式

· 阅读需 1 分钟
素明诚
Full stack development
参数类型Swagger 描述说明示例(传参形式)
Query Parameters参数在 URL 的查询部分使用 ? 开始,并使用 & 分隔多个参数http://sumingcheng.cn/api?param=value
Path Parameters参数是 URL 路径的一部分路径参数通常用花括号 表示,并在实际请求中替换为实际值http://sumingcheng.cn/api/users/{userId}
Request Body参数在 HTTP 请求体中通常用于 POST、PUT 或 PATCH 请求,可以是 JSON、XML 等格式{ "username": "John", "email": "john@sumingcheng.cn" }
Header Parameters参数在 HTTP 请求头中用于传递额外的元数据,如身份验证令牌、客户端可接受的内容类型等Authorization: Bearer token_value
Cookie Parameters参数在 HTTP cookies 中用于会话管理、用户跟踪等sessionId=12345
Form Data Parameters参数在表单数据中通常用于提交 HTML 表单,使用 application/x-www-form-urlencoded 或 multipart/form-data 格式username=John&email=john@sumingcheng.cn

Indexdts 为什么要写 export

· 阅读需 2 分钟
素明诚
Full stack development

在 TypeScript 中,当你在一个 .ts 或 .d.ts 文件中使用 export 关键字,该文件会被视为一个模块。相反地,如果文件中没有 export 或 import 语句,那么它会被视为一个全局脚本,其中的所有声明都会被加入到全局命名空间。

在 .d.ts 文件中,通常我们希望为模块或库定义类型,而不是为全局命名空间添加内容。

但是,有时候,我们可能只是想声明一些全局的接口或类型,而不实际导出任何内容。在这种情况下,只写类型声明可能会导致 TypeScript 将这个文件误认为是全局脚本。

为了避免这个问题,但又不实际导出任何东西,我们可以使用一个空的 export {} 语句。这会将文件标记为模块,从而确保文件内的所有声明都仅在该模块中可见,而不是被加入到全局命名空间。

所以,export {} 的主要目的是确保 .d.ts 文件被视为一个模块,而不是全局脚本,从而避免意外地将声明加入到全局命名空间。

深入理解 await 与 async

· 阅读需 4 分钟
素明诚
Full stack development

1. 基本概念​

什么是 async 函数?​

async 函数是一个返回 Promise 对象的函数。在函数内部,你可以使用 await 关键字来等待一个 Promise。

什么是 await 关键字?​

await 关键字只能在 async 函数内部使用,它会暂停当前函数的执行直到 Promise 完成,并返回结果。

async 函数返回什么类型的值?​

async 函数总是返回一个 Promise。


2. 错误处理​

如何在使用 await 时处理错误?​

使用 try/catch 结构。

try/catch 在 async/await 中的作用是什么?​

它用于捕获 await 表达式中的错误或在 async 函数内部抛出的错误。

如何处理多个 await 调用中的并发错误?​

  1. 如果你使用 Promise.all() 来同时等待多个 Promise,它会在其中一个 Promise 失败时立即失败。
  2. 你可以在这种情况下使用 try/catch 来捕获错误。

3. 并发性​

如何并发地执行多个 await 操作?​

你可以使用 Promise.all() 或者 Promise.allSettled() 来并发执行多个 Promise。

Promise.all()和 Promise.race()在与 async/await 结合时有什么作用?​

  1. Promise.all() 允许你并发等待多个 Promise,它返回一个 Promise,该 Promise 在所有的 Promise 完成后完成。
  2. Promise.race() 返回一个 Promise,该 Promise 在第一个 Promise 完成后完成。

4. 与 Promise 的关系​

async/await 和 Promises 之间有什么关系?​

async/await 是基于 Promise 的,并提供了一种更简洁、更直观的方式来处理 Promise。

在 async 函数中返回一个值等价于哪种 Promise 操作?​

  1. 返回一个值等价于 Promise.resolve()。
  2. 抛出错误等价于 Promise.reject()。

5. 执行顺序​

描述一个包含多个 await 语句的 async 函数的执行顺序。​

函数会从顶部开始执行,当遇到第一个 await 语句时暂停,直到该 Promise 完成。然后,函数会继续执行,直到遇到下一个 await 或函数结束。

何时使用 await 和何时避免使用?​

  1. 当你需要按顺序执行异步操作时,使用 await。
  2. 当你可以并发执行多个操作时,使用 Promise.all() 或其他类似的方法。

6. 错误和陷阱​

描述在使用 async/await 时可能遇到的常见错误或陷阱。​

  1. 忘记 await。
  2. 在非 async 函数内部使用 await。
  3. 在循环中不正确地使用 await 导致不必要的顺序执行。
  4. 未处理的异步错误。

如何避免“忘记 await”的问题?​

使用 TypeScript 或 ESLint,它们可以检测这种常见错误。


7. 实际应用​

这通常涉及将给定代码片段转换为使用 async/await 或 Promises。这要根据具体的代码片段来决定。


8. 浏览器和 Node.js 支持​

哪些版本的 Node.js 和浏览器开始支持 async/await?​

Node.js 从 7.6.0 版本开始支持 async/await。大多数现代浏览器都支持它,但旧版本的浏览器可能不支持。

在不支持 async/await 的环境中,如何使用它?​

你可以使用 Babel 或 TypeScript 这样的转译器将 async/await 转换为 ES5 或 ES6 JavaScript。


9. 性能和最佳实践​

async/await 是否比纯 Promises 更慢?​

在大多数情况下,性能差异是可以忽略的。但 async/await 可能会带来一些额外的运行时开销。

何时应该使用 async/await,何时应该使用 Promises?​

  1. 当你需要更简洁和可读的代码时,使用 async/await。
  2. 对于复杂的异步逻辑或需要更细粒度控制的场景,使用 Promises。

10. 其他特性和提议​

对于 async iterators 和 for-await-of 的了解。​

  1. async iterators 是一种可以异步生成值的迭代器。
  2. for-await-of 是一种循环结构,允许你等待 async iterator 的每个值。

了解其他与 async/await 相关的 ES 提议,如顶级 await。​

顶级 await 是一个提议,允许你在模块的顶级使用 await,而不是仅在 async 函数中。

JavaScript事件环宏任务和微任务

· 阅读需 1 分钟
素明诚
Full stack development

JavaScript 是单线程语言,它在一个事件环中处理所有任务。事件环是一个循环,在每一轮中,JavaScript 引擎会处理所有排队的任务,包括计算、事件处理以及其他任务。

在 JavaScript 中,任务可以分为两种:宏任务和微任务。

639f1f477d84ad86767cd703ac0b5fd1

宏任务指的是任务队列中的主任务,主要包括:

  • 整体代码(script)
  • 显式的定时器(setTimeout,setInterval)
  • UI 交互事件(click)
  • 网络请求事件

微任务指的是任务队列中的次要任务,主要包括:

  • process.nextTick()
  • Promise
  • Object.observe
  • MutationObserver

JavaScript 引擎在处理完一轮宏任务后,再处理当前微任务队列中的所有任务,直到当前微任务队列中的所有任务都处理完成,再回到主线程,处理下一轮宏任务。这样的循环继续进行,直到所有任务都处理完成。

pomxml 文件主要内容

· 阅读需 2 分钟
素明诚
Full stack development

项目基本信息:

  • modelVersion: POM 模型的版本。
  • groupId, artifactId, version: 定义了项目的坐标。
  • name: 项目的名称。
  • url: 项目的 URL。
  • description: 项目的描述。
  1. 项目属性 (properties): 定义了一些可以在整个pom.xml文件中重复使用的属性。
  2. 依赖管理 (dependencyManagement): 定义了项目中所有可能用到的依赖及其版本。
  3. 模块 (modules): 列出了项目的所有子模块。
  4. 构建 (build): 定义了项目的构建配置,如使用的插件。
  5. 仓库 (repositories 和 pluginRepositories): 定义了 Maven 从哪里获取依赖和插件。
  6. 依赖 (dependencies): 列出了项目直接依赖的库。
  7. 父项目 (parent): 定义了项目的父项目,从中继承某些配置。
  8. 配置 (profiles): 定义了多个构建配置。
  9. 报告插件 (reporting): 定义了生成项目报告的插件。
  10. 分发管理 (distributionManagement): 定义了项目的发布配置。
  11. 属性 (properties): 可以定义任意的键值对,这些键值对可以在整个pom.xml文件中使用。
  12. 组织信息 (organization): 提供有关项目所属组织的信息。
  13. 开发者和贡献者 (developers 和 contributors): 列出了项目的主要开发者和其他贡献者。
  14. 许可证 (licenses): 描述了项目的许可证信息。

实现 EventHub

· 阅读需 4 分钟
素明诚
Full stack development

当面试官让你实现 EventHub 或类似的事件系统时,他们主要考察以下几点:

  1. 基本的编程能力:这是显而易见的。你能否写出工作的代码,是否熟悉语言特性和基础概念。
  2. 数据结构的知识:如何高效地存储和检索事件监听器是关键。通常,哈希表(在 JavaScript 中是对象或 Map)是一个合适的选择。
  3. 设计模式:事件模型是观察者模式的一个实例。面试官可能希望你能识别并实现这一点。
  4. 异步编程:JavaScript 的事件和异步性质是其核心特性之一。对 Promise、async/await 或其他异步机制的了解和正确使用会是一个加分项。
  5. 错误处理:如何处理监听器中的错误,如何通知其他部分的应用程序,这都是重要的设计决策。
  6. 代码的可维护性和可读性:清晰、结构化、有注释的代码总是受到欢迎的。
  7. API 的设计:一个好的 EventHub 实现不仅要功能齐全,而且要易于使用。面试官可能会评估你设计的 API 是否直观和用户友好。
  8. 性能和优化:虽然初级实现可能不需要考虑这些,但面试官可能会问及性能问题,或者询问如何优化代码。
  9. 扩展性和灵活性:除了基本功能外,面试官可能还会询问如何扩展你的实现,例如添加事件组、命名空间、中间件等。
  10. 测试:对于一些高级职位或特定的公司,面试官可能会询问如何为你的 EventHub 实现编写测试。

如何编写​

  1. 首先编写最基础的方法 on、emit、off,实现最基本的事件监听和触发功能
  2. 接着再在此基础上进行完善、once、bind、链式调用等
  3. 最后增加一些查看获取事件的方法

EventHub 案例​

/*
* on 方法:注册事件监听器,本质就是往数组里面添加回调函数
* emit 方法:发射事件,本质就是遍历数组,执行回调函数
* off 方法:移除事件监听器,本质就是从数组里面移除回调函数
* once 方法:注册只触发一次的事件监听器,本质就是在回调函数执行后,移除该回调函数
* bind 方法:在指定上下文中注册事件监听器,本质就是利用 on 方法注册回调函数时,使用 bind 方法绑定上下文
* 链式调用:所有方法都返回 this,就支持链式调用
* */

class EventHub {
constructor() {
// 存储所有的事件及其对应的回调
this.events = {}
}

// 注册事件监听器
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
return this // 支持链式调用
}

// 发射事件
emit(event, ...args) {
if (this.hasListener(event)) {
new Promise((resolve, reject) => {
const listeners = [...this.events[event]]
for (let callback of listeners) {
try {
callback(...args)
} catch (error) {
// 如果 error 事件的监听器也出错,直接拒绝 Promise
if (event === 'error') {
reject(error)
return
} else {
this.emit('error', error)
}
}
}
resolve()
}).catch(error => {
console.error('Error in emit method:', error)
})
}
return this
}

// 移除事件监听器
off(event, callback) {
if (this.hasListener(event)) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
return this
}

// 注册只触发一次的事件监听器
once(event, callback) {
const wrappedCallback = (...args) => {
callback(...args)
this.off(event, wrappedCallback)
}
return this.on(event, wrappedCallback) // 利用 on 方法进行注册
}

// 在指定上下文中注册事件监听器
bind(event, callback, context) {
return this.on(event, callback.bind(context))
}

// 检查指定事件是否有监听器
hasListener(event) {
return this.events[event] && this.events[event].length > 0
}

// 获取所有已注册的事件类型
getEventTypes() {
return Object.keys(this.events)
}
}

// 使用示例
// 1. 获取 EventHub 的实例
const eventHub = new EventHub()

// 2. 注册事件监听器
eventHub.on('data', data => {
console.log(data)
throw new Error('Sample error from data listener')
})

eventHub.once('data', data => {
console.log(`Once: ${data}`)
})

const errorCallback = error => {
console.log(`Error: ${error.message}`)
}
eventHub.on('error', errorCallback)

const user = {
name: 'Alice',
showName() {
console.log(`Hello, my name is ${this.name}`)
}
}
eventHub.bind('showName', user.showName, user)

// 3. 发射事件
console.log('Before emit')
eventHub.emit('data', 'Some data')
eventHub.emit('showName')
console.log('After emit')

// 4. 移除事件监听器
eventHub.off('error', errorCallback)

// 5. 检查是否有监听器
console.log('Has data listener:', eventHub.hasListener('data')) // true
console.log('Has error listener:', eventHub.hasListener('error')) // false

// 6. 获取所有的事件类型
console.log('Event types:', eventHub.getEventTypes()) // ['data', 'error', 'showName']

使用 Jest 测试 EventHub​

const EventHub = require('./EventHub');  // 调整路径以匹配你的EventHub文件位置

describe('EventHub', () => {
let eventHub;

beforeEach(() => {
eventHub = new EventHub();
});

test('should register and trigger an event', () => {
const callback = jest.fn();
eventHub.on('testEvent', callback);

eventHub.emit('testEvent', 'payload');

expect(callback).toHaveBeenCalledWith('payload');
});

test('should register and trigger an event only once', () => {
const callback = jest.fn();
eventHub.once('testEvent', callback);

eventHub.emit('testEvent', 'payload');
eventHub.emit('testEvent', 'payload');

expect(callback).toHaveBeenCalledTimes(1);
});

test('should unregister an event', () => {
const callback = jest.fn();
eventHub.on('testEvent', callback);
eventHub.off('testEvent', callback);

eventHub.emit('testEvent', 'payload');

expect(callback).not.toHaveBeenCalled();
});
});

伪元素和伪类的区别

· 阅读需 2 分钟
素明诚
Full stack development

1. 伪元素 (Pseudo-elements)​

  • 定义: 伪元素允许你为某一部分的元素设置样式,如选择元素的某一部分或在元素的内容前后插入内容。
  • 语法: 在 CSS3 中,伪元素使用双冒号 :: 前缀,例如 ::before 和 ::after。
  • 常见伪元素: ::before, ::after, ::first-line, ::first-letter, ::selection。
  • 用途: 主要用于插入内容和为元素的某一部分设置样式。

2. 伪类 (Pseudo-classes)​

  • 定义: 伪类允许你基于元素的特定状态(如悬停或被点击)或其与其他元素的关系来应用样式。
  • 语法: 伪类使用单冒号 : 前缀,例如 :hover 和 :active。
  • 常见伪类: :hover, :active, :focus, :first-child, :last-child, :nth-child(), :not(), :checked 等。
  • 用途: 主要用于描述元素的特定状态或其与其他元素的关系。

主要区别:​

  1. 目的: 伪元素主要用于插入和修改元素的部分内容,而伪类则用于描述元素的状态或与其他元素的关系。
  2. 语法: 在 CSS3 中,伪元素使用双冒号 ::,而伪类使用单冒号 :。
  3. 应用数量: 一个元素可以同时应用多个伪类,但只能应用一个伪元素(例如,你不能同时应用 ::before 和 ::after 于同一元素上,但可以应用 :hover 和 :active)。

个人觉得,伪元素应该叫辅助元素,伪类应该叫状态元素更贴切