跳到主要内容

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 关键字,该文件会被视为一个模块。相反地,如果文件中没有 exportimport 语句,那么它会被视为一个全局脚本,其中的所有声明都会被加入到全局命名空间。

.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/awaitPromises。这要根据具体的代码片段来决定。


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. 仓库 (repositoriespluginRepositories): 定义了 Maven 从哪里获取依赖和插件。
  6. 依赖 (dependencies): 列出了项目直接依赖的库。
  7. 父项目 (parent): 定义了项目的父项目,从中继承某些配置。
  8. 配置 (profiles): 定义了多个构建配置。
  9. 报告插件 (reporting): 定义了生成项目报告的插件。
  10. 分发管理 (distributionManagement): 定义了项目的发布配置。
  11. 属性 (properties): 可以定义任意的键值对,这些键值对可以在整个pom.xml文件中使用。
  12. 组织信息 (organization): 提供有关项目所属组织的信息。
  13. 开发者和贡献者 (developerscontributors): 列出了项目的主要开发者和其他贡献者。
  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)。

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