Hutool Sftp 负载均衡踩坑记

踩坑背景

最近有个项目使用到了Sftp,部署的时候发现了一个奇怪的现象,当sftp通过ip直接连接的时候,程序是一切正常的,在使用运维给的LB地址时,老是会出现下面这个报错

cn.hutool.extra.ssh.JschRuntimeException: JschException: Session.connect: java.net.SocketException: Connection reset
    at cn.hutool.extra.ssh.JschUtil.openSession(JschUtil.java:116)
    at cn.hutool.extra.ssh.JschUtil.openSession(JschUtil.java:97)
    at cn.hutool.extra.ssh.JschSessionPool.lambda$getSession$64b21fc$1(JschSessionPool.java:45)
    at cn.hutool.core.lang.SimpleCache.get(SimpleCache.java:112)
    at cn.hutool.extra.ssh.JschUtil.getSession(JschUtil.java:55)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:166)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:93)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:80)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:70)
    at cn.hutool.extra.ssh.Sftp.init(Sftp.java:56)
    at 

报错的同时,在服务器上尝试使用命令行LB地址连接Sftp却是一切正常的!!!同时经过排查发现,当程序刚启动尝试第一次连接的时候并没有出现这个奇怪的问题,正当我看这个奇怪的堆栈百思不得其解的时候,突然注意到了一个奇怪的打印

at cn.hutool.core.lang.SimpleCache.get(SimpleCache.java:112)

经过排查发现,hutool的Sftp工具类是默认开启了一个JschSessionPool的连接池,默认的直接使用 new Sftp(…) 或者 JschUtil.getSession(…) 都会把新建立的连接放到 JschSessionPool 这个连接池中,而恰恰就是这个连接池导致的问题!!!

修改方案

如果你也是使用的hutool Sftp工具类且涉及一些比较复杂的网络场景,建议跳过Hutool的Sftp自带的连接池逻辑,改成直接使用 JschUtil.createSession(…) 手动建立连接并且手动释放,这种场景反而更好做一些细粒度的控制

教训总结

在使用一个工具类前,必须要大致了解其底层实现逻辑以及相关issue,避免老是踩前辈们踩过的坑!!!!!!!!!!

MYSQL

1. 聚簇索引(Clustered Index)

聚簇索引是一种将数据存储和索引结合的索引形式。在 MySQL 中,聚簇索引是一种数据存储方式,表中的行数据存储在索引本身的叶子节点上。因此,聚簇索引上的叶子节点包含了实际的数据行。换句话说,数据行按照聚簇索引的顺序物理存储在磁盘上,索引与数据“聚合”在一起。

聚簇索引的特点包括:

  • InnoDB 默认主键作为聚簇索引:在 InnoDB 存储引擎中,聚簇索引总是基于主键。如果没有定义主键,InnoDB 会尝试选择一个唯一的非空索引作为聚簇索引;如果都没有,就会创建一个隐藏的 6 字节的唯一 ID 列作为聚簇索引。
  • 每张表只能有一个聚簇索引:因为表数据只能按一个顺序存储,因此一个表只能有一个聚簇索引。

优点:

  • 数据访问快:因为数据和索引存储在一起,按聚簇索引查找数据非常快。
  • 范围查询性能高:对于范围查询,聚簇索引可以快速定位到数据的起始点,减少磁盘 I/O。

缺点:

  • 插入、删除和更新成本高:因为数据行的物理顺序和聚簇索引的顺序一致,插入、删除、更新操作会影响数据行的存储顺序,可能会导致频繁的页分裂和页合并,增加了操作的复杂性和成本。
  • 占用更多的空间:因为聚簇索引包含了数据本身,如果表很大,索引的体积也会很大。

2. 非聚簇索引(Non-clustered Index)

非聚簇索引是一种独立于数据的索引结构,索引和数据存储在不同的物理位置上。非聚簇索引的叶子节点只包含键值以及指向数据行的指针(如行指针或主键值),而不包含实际的数据行。

非聚簇索引的特点包括:

  • 索引和数据分离:非聚簇索引的叶子节点包含了指向数据的引用,而不包含数据本身。
  • 可以有多个非聚簇索引:因为非聚簇索引不影响数据的物理存储顺序,因此一个表可以有多个非聚簇索引。
  • 适用于多种查询:非聚簇索引适合频繁查询的列,尤其是那些不用于排序或范围查询的列。

优点:

  • 更新效率高:因为非聚簇索引不影响数据行的存储顺序,数据插入、删除和更新操作不会影响索引的结构。
  • 支持多列索引:非聚簇索引可以更灵活地支持多个查询需求。

缺点:

  • 数据访问稍慢:由于非聚簇索引指向数据行的位置,而不是直接存储数据,因此需要多一次查找操作。
  • 可能需要回表:对于非聚簇索引查询,如果查询的列不在索引中,则需要回表查找数据。

SQL-CTE递归查询子ID

场景1: 查询某个节点的所有子孙id

idparent_idname
1NULLRoot
21Child_A
32Child_B
父子id结构
WITH RECURSIVE descendants AS (
    -- 初始条件
    SELECT id, parent_id, name
    FROM my_table
    WHERE id = 1
    UNION ALL
    -- 递归条件
    SELECT t.id, t.parent_id, t.name
    FROM my_table t
    JOIN descendants d ON t.parent_id = d.id
)
SELECT id, name FROM descendants WHERE id <> 1;

场景2: 查询某个节点的所有子孙id,构建children数组结构JSON

用sql来说似乎没法实现

Postgres 分布式拓展 citus 初探

citus是什么

citus是postgres的分布式拓展,它能够以非常简单的配置实现分布式的postgres数据库并实扩容,还能够将SQL进行并行化处理,充分利用多台机器的资源,使用于大量数据的实时查询场景。

citus的基本概念

节点:Coordinator Node(协调节点)、Worker Node(工作节点)

表:Distributed Table(分布式表)、 Reference Table(引用表) 、Local Tables (本地表)

citus的节点分为工作节点和协调节点。

协调节点是我们连接使用的主节点,它的主要职责是连接所有的工作节点分发和协调数据,不会保存分布式表的数据。

工作节点是实际保存数据和执行SQL的主体,协调节点会将SQL处理后分发到对应的节点执行任务。

citus的表有三种类型:分布式表、引用表、本地表。

分布式表,顾名思义就是实现分布式存储的表,它的数据是根据实现创建分布式表时指定的字段作为key进行哈希,水平分布在所有的数据节点中,是citus最常用的分布式表。

引用表也是分布式表的一种,它的数据完整会被保存所有的工作节点中,所有工作节点都拥有该表的全部数据。

本地表不是分布式表,它是存储在协调节点上面的表,和普通postgres数据库的表一样。

实战体验

准备一台主节点、若干工作节点

我这边正好有四台Hosthatch虚拟机,都重装成debian11用来搭建实战。

协调节点 192.168.10.202

工作节点1 192.168.10.101

工作节点2 192.168.10.102

工作节点3 192.168.10.103

先安装postgres和citus拓展(所有节点都要执行下面的步骤)

apt update &amp;&amp; apt install -y sudo gnupg2

#添加citus源
curl https://install.citusdata.com/community/deb.sh | sudo bash
#添加postgresql源,我们要安装最新的postgresql-15
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

#更新源
sudo apt-get update

#安装postgresql-15、以及对应版本的citus拓展
sudo apt-get -y install postgresql-15 postgresql-15-citus-11.1

修改postgresql监听IP、添加四台节点互相信任(所有节点都要执行)

# 预加载citus扩展
sudo pg_conftool 15 main set shared_preload_libraries citus
# 修改监听IP,这里我直接改了 *,如果是暴露在公网的建议只监听内网ip
sudo pg_conftool 15 main set listen_addresses '*'
# 修改添加四台节点互信
sudo vim /etc/postgresql/15/main/pg_hba.conf
#在最底下追加下面内容
host    all             all              192.168.10.202/24                trust
host    all             all              192.168.10.101/24                trust
host    all             all              192.168.10.102/24                trust
host    all             all              192.168.10.103/24                trust

#重启 
systemctl restart postgresql

完成了上面的步骤后,我们开始选择一个数据库添加citus拓展(所有节点都要执行)

# 我这边直接选择的postgres,如果要使用不同的库,所有节点都需要有该库存在
sudo -i -u postgres psql -c "CREATE EXTENSION citus;"

接下来在协调节点中执行相关函数,设置协调节点和添加工作节点(只在协调节点运行)

#设置协调节点 IP端口根据实际情况来
sudo -i -u postgres psql -c "SELECT citus_set_coordinator_host('192.168.10.202', 5432);"
#添加工作节点
sudo -i -u postgres psql -c "SELECT * from citus_add_node('192.168.10.101', 5432);"
sudo -i -u postgres psql -c "SELECT * from citus_add_node('192.168.10.102', 5432);"
sudo -i -u postgres psql -c "SELECT * from citus_add_node('192.168.10.103', 5432);"

#查看活跃的工作节点
sudo -i -u postgres psql -c "SELECT * FROM citus_get_active_worker_nodes();"

到此为止,我们前置配置相关的工作已经完成了,接下来就是创建分布式表并感受分布式的快乐了,比如下面的流程

#后续都是在协调节点执行
#创建表结构
CREATE TABLE IF NOT EXISTS key (
                                   id uuid,
                                   word varchar NULL,
                                   url varchar NULL,
                                   description varchar null,
                                   PRIMARY KEY (id)
);
#调用citus提供的函数创建分布式表 第一个参数是表名 第二个参数是依据该字段进行分片,将数据分配到所有的工作节点
SELECT create_distributed_table('key', 'id');

#这样就建立好了一个分布式表,使用它也是让程序直接连接协调节点,就像平常连接一个数据库那样直接使用即可

笔记-HTTPS握手流程

HTTPS(超文本传输安全协议)的连接流程结合了传输层的TCP三次握手和TLS(传输层安全性协议)的握手过程来提供一个安全的连接。这个过程可以大致分为以下几个步骤:

1. TCP三次握手

这是任何HTTP或HTTPS连接开始的地方。客户端和服务器之间建立一个TCP连接,这是一个可靠的连接,确保了数据包正确无误地到达。

2. TLS握手

在TCP连接成功建立之后,TLS握手过程开始,目的是安全地交换密钥,验证服务器,并且建立加密通道。

a) 客户端Hello(Client Hello)

客户端发送一个“Client Hello”消息到服务器,包含:

  • 客户端支持的TLS版本。
  • 可接受的加密算法(密码套件)列表。
  • 一个客户端随机数(Client Random),之后用于生成加密密钥。

b) 服务器Hello(Server Hello)

服务器响应一个“Server Hello”消息,包含:

  • 选择的TLS版本和密码套件。
  • 一个服务器随机数(Server Random)。
  • 服务器证书(通常是X.509格式)。

c) 服务器证书验证

客户端验证服务器证书的有效性,它可能包括:

  • 确认证书由可信的证书颁发机构(CA)签名。
  • 检查证书的有效期。
  • 确认证书中的域名与正在连接的服务器域名一致。

d) 密钥交换

客户端使用从服务器证书中提取的公钥,发送预主密钥(Pre-Master Secret),这通常是加密的。

e) 会话密钥生成

客户端和服务器都使用客户端随机数、服务器随机数和预主密钥生成相同的会话密钥,这将用于此后的对话加密。

f) 客户端完成(Client Finished)

客户端发送一个“Finished”消息,这通常是之前所有消息的加密和散列值,用于服务器验证。

g) 服务器完成(Server Finished)

服务器同样发送一个“Finished”消息,客户端验证这个消息以确保没有中间人攻击。

3. 加密通信

HTTPS使用的是一种称为公钥基础设施(PKI)的体系结构,在这种体系结构中,确实使用了公钥和私钥机制,但它结合了对称加密和非对称加密的优点来提供安全通信。

  1. 非对称加密
    • 在初始的握手阶段,服务器会向客户端发送其公钥作为数字证书的一部分。
    • 客户端将使用此公钥加密信息(如预主密钥),然后发送回服务器。
    • 只有持有对应私钥的服务器才能解密这些信息,这确保了即使这些信息被截获,没有私钥也无法解密。
    • 这个过程是非对称的,因为加密(公钥)和解密(私钥)使用不同的密钥。
  2. 对称加密
    • 一旦非对称步骤完成,客户端和服务器就会使用交换的信息来生成对称的会话密钥。
    • 后续的通信将使用这个对称密钥进行加密和解密,这意味着客户端和服务器都使用同一个密钥。
    • 对称加密比非对称加密更快,适合大量数据的加密通信。

笔记-TCP的三次握手四次挥手

TCP三次握手例子

假设你的电脑(客户端A)尝试通过TCP连接到一个网站的服务器(服务器B)。服务器的IP地址是192.168.1.1,客户端的IP地址是192.168.1.2,而你想要访问的服务(比如一个网页)是通过标准的HTTP服务端口80提供的。

  1. SYN步骤:
    • 客户端A(192.168.1.2:12345)生成一个随机的初始序列号,比如1000,并向服务器B的80端口发送一个TCP段,其SYN位设置为1。
    • 此时的TCP包头部信息大致如下:makefileCopy code源地址:192.168.1.2:12345 目的地址:192.168.1.1:80 Seq=1000 ACK=0 Flags=[SYN]
    • 服务器B接收到这个请求。
  2. SYN-ACK步骤:
    • 服务器B(192.168.1.1:80)收到请求后,会从它自己的端口回应客户端A。它也会生成一个随机的初始序列号,比如5000,并且把确认号设置为客户端A的序列号加1(也就是1001)。
    • 服务器B发送的TCP包头部信息大致如下:makefileCopy code源地址:192.168.1.1:80 目的地址:192.168.1.2:12345 Seq=5000 ACK=1001 Flags=[SYN, ACK]
    • 客户端A接收到这个响应。
  3. ACK步骤:
    • 客户端A(192.168.1.2:12345)再次发送一个TCP段给服务器B,ACK位设置为1,序列号为1001(表示客户端A准备好接收服务器B的下一个字节),确认号为5001(表示服务器B发送的数据已被确认,服务器可以发送下一个字节了)。
    • 此时客户端A发送的TCP包头部信息大致如下:makefileCopy code源地址:192.168.1.2:12345 目的地址:192.168.1.1:80 Seq=1001 ACK=5001 Flags=[ACK]
    • 服务器B接收到这个确认。

TCP三次握手作用包括:

  1. 同步序列号:由于TCP是基于字节流的,各个数据包的顺序非常重要。三次握手可以同步连接双方的初始序列号,从而保证数据传输的顺序和完整性。
  2. 确认双方的接收和发送能力:三次握手确保了双方都能够接收和发送数据。首次握手确认了服务器的接收能力,第二次握手确认了服务器的发送能力以及客户端的接收能力,最后一次握手则确认了客户端的发送能力。
  3. 防止旧的连接请求造成混乱:网络中可能存在旧的连接请求数据包(因为延迟或重发),这些过时的数据包如果被接收,可能会造成错误的连接建立。三次握手通过确认和序列号机制,使得旧的连接请求不会被错误地接受。
  4. 资源分配:在三次握手过程中,TCP堆栈会分配缓冲区和变量以跟踪连接状态。通过这样的过程,只有在双方都确认连接请求之后,才会完成资源分配,避免了因为未完成连接而造成的资源浪费。

三次握手是对建立TCP连接过程的最小化要求,它可以有效地启动一个双向通信会话,同时避免了一些常见的网络通信问题。如果减少到两次握手,那么就无法解决上述的第三个问题,也就是说,如果网络中存在延迟的连接请求,那么这样的请求可能会导致不必要的连接建立,从而造成混乱。三次握手是在效率和可靠性之间的一个妥协。

四次挥手断开TCP连接的例子:

假设客户端A与服务器B之间建立了一个TCP连接,并且在此期间它们已经交换了一些数据。现在,客户端A决定关闭这个连接。以下是关闭过程的详细步骤和假设的序列号及确认号。

  1. 第一次挥手
    • 客户端A发送一个FIN标志的段给服务器B,指示A已经完成了数据的发送。
    • 假设客户端A之前发送的最后一个数据段的序列号是10000。
    • 客户端A的FIN段的序列号将会是10001(因为TCP协议规定FIN标志的段也消耗一个序列号)。
    • 如果服务器B之前发送给客户端A的最后一个数据段的序列号是5000,那么客户端A发送的ACK确认号将是5001,确认它已经收到了直到序列号5000的所有数据。
    • 段信息如下:cssCopy code源地址:客户端A 目的地址:服务器B Seq=10001 Ack=5001 Flags=[FIN, ACK]
  2. 第二次挥手
    • 服务器B收到客户端A的FIN段,发送一个ACK段回应,确认收到A的结束请求。
    • 服务器B的ACK段的确认号将是客户端A的序列号加1,也就是10002,因为它需要确认收到了包含FIN标志的那个序列号。
    • 此时,服务器B还没有准备好关闭连接或者可能仍有数据需要发送给客户端A。
    • 段信息如下:cssCopy code源地址:服务器B 目的地址:客户端A Seq=服务器B当前的序列号 Ack=10002 Flags=[ACK]
  3. 第三次挥手
    • 服务器B已完成数据的发送,并且准备关闭连接,发送一个FIN标志的段给客户端A。
    • 假设服务器B之前发送的最后一个数据段的序列号是8000。
    • 服务器B发送的FIN段的序列号将会是8001。
    • 服务器B在发送FIN之前也已经收到了客户端A之前的所有数据,所以ACK确认号将是客户端A之前的确认号,也就是10002。
    • 段信息如下:cssCopy code源地址:服务器B 目的地址:客户端A Seq=8001 Ack=10002 Flags=[FIN, ACK]
  4. 第四次挥手
    • 客户端A收到服务器B的FIN段,发送一个ACK段回应,确认收到B的结束请求。
    • 客户端A的ACK段的确认号将是服务器B的序列号加1,也就是8002。
    • 此时客户端A会等待足够的时间确保服务器B收到了这个ACK段,这个时间通常是该连接的往返时延的两倍。
    • 殅信息如下:cssCopy code源地址:客户端A 目的地址:服务器B Seq=10002 Ack=8002 Flags=[ACK]

TCP的四次挥手(Four-way Handshake)断开连接的过程比建立连接的三次握手(Three-way Handshake)要复杂,这主要是因为TCP连接是全双工的,意味着每个方向的通信是独立的。这里是为什么需要四次挥手的一些原因:

  1. 全双工通信:在TCP连接中,数据传输是全双工的,即数据可以在两个方向上同时进行。因此,每个方向都必须单独关闭,这就需要两个FIN标志和两个ACK标志。
  2. 确保数据完全传输:TCP连接的一方(比如客户端)发送FIN标志仅表示它没有更多的数据要发送,但是它仍然可以接收数据。服务器在收到这个FIN之后,可能仍然有数据要发送给客户端。服务器必须确保所有传出的数据都被客户端接收到,然后才能安全地发送它自己的FIN标志来关闭连接的另一个方向。
  3. 防止数据丢失:四次挥手允许每一方确认它们已经接收到对方发送的最后一个包。这意味着如果任何FIN或ACK在传输过程中丢失,连接不会立即关闭,从而允许重传丢失的包,确保连接的可靠终止。
  4. TIME_WAIT状态:客户端发送最后一个ACK后,它还需要等待足够的时间(称为2MSL,Maximum Segment Lifetime)以确保服务器收到了这个ACK。如果服务器没有收到ACK,它将重新发送FIN包。TIME_WAIT状态确保了旧的连接残留的数据包不会在新的连接中出现,从而避免可能的混淆。

总结来说,四次挥手的过程确保了连接的双方都能够完全完成数据传输,并且优雅地关闭连接,没有任何一方突然中断连接导致数据丢失。这也是为什么不能简化为三次或更少次数的原因,因为TCP设计的优先考虑是确保数据传输的可靠性和完整性。

笔记-Java针对字节码的优化

编译器生成字节码时的优化

编译器在生成字节码时可以进行一些优化,以提高代码的执行效率和性能。以下是一些常见的优化技术:

  1. 常量折叠:编译器可以在编译时将常量表达式计算出来,将其结果作为字节码中的常量。这样可以避免在运行时重复计算相同的表达式。
  2. 循环展开:编译器可以将循环展开,将循环体中的代码复制多次以减少循环的迭代次数。这样可以减少循环控制的开销,提高程序的执行速度。
  3. 变量内联:编译器可以将某些函数调用替换为直接使用函数体中的代码。这样可以避免函数调用的开销,减少程序的运行时间。
  4. 公共子表达式消除:编译器可以识别出相同的子表达式,并将其计算结果存储在临时变量中。这样可以避免重复计算相同的表达式,提高程序的执行效率。
  5. 死代码消除:编译器可以识别出不会被执行的代码,将其从生成的字节码中删除。这样可以减少字节码的大小,提高程序的执行效率。
  6. 消除冗余检查:消除不必要的安全检查、空指针检查等。

这些优化技术可以帮助编译器生成更高效的字节码,从而提高代码的执行效率和性能。编译器根据代码的特性和优化策略的选择,可以进行不同程度的优化。

JVM在运行时执行的一些关键优化:

即时编译(JIT)和适应性优化

  • 方法热点探测:JVM监控方法和循环的执行频率,将经常执行的代码(热点代码)识别出来进行优化。
  • 适应性优化:JVM能够根据程序的实际运行情况动态调整优化策略。
  • 多级优化:JVM可能会采用多级别的优化,对不同的代码应用不同程度的优化。

编译器优化

  • 方法内联:将方法调用替换为方法体本身,减少方法调用的开销。
  • 循环优化:包括循环展开、循环不变式外提等技术来减少循环开销。
  • 逃逸分析:分析对象是否在方法或线程外部被引用,如果没有“逃逸”,可能在栈上分配内存,避免堆分配。
  • 消除冗余检查:消除不必要的安全检查、空指针检查等。
  • 公共子表达式消除:如果同一个表达式被多次计算,并且每次计算的结果都相同,JIT编译器会尝试只计算一次。

垃圾回收(GC)优化

  • 分代收集:根据对象的生命周期将堆内存分成几块,如年轻代、老年代等,以不同方式进行垃圾回收。
  • 垃圾回收算法:采用多种算法,如标记-清除、标记-整理、复制算法等,优化GC性能和响应时间。
  • 并发和并行垃圾回收:在多核处理器上并行进行垃圾回收,以及在应用线程运行的同时并发执行垃圾回收。

锁和同步优化

  • 轻量级锁:当锁未竞争时,使用更高效的算法来减少线程阻塞。
  • 锁消除:编译器识别并去除不需要的同步。
  • 锁粗化:如果在一系列操作中多次获取和释放同一个锁,则可能将锁的范围扩展到整个操作序列,减少锁操作的开销。
  • 偏向锁:在单线程访问的情况下降低锁的竞争开销。

运行时性能监控与反馈

  • 动态编译器反馈:JIT编译器可以根据代码的执行情况收集信息,并利用这些信息进行更好的优化。
  • 反优化:在一些情况下,之前做出的优化可能不再有效,JVM可以将已经编译的代码恢复到未优化的状态。

JVM的优化是一个持续的过程,随着程序的运行,JVM会逐渐学习并应用越来越多的优化策略,以期达到最优性能。

笔记-JAVA 基本数据类型和对应封装类思想


Java中的基本数据类型和它们的封装类(有时被称作包装类)之间存在一些关键的不同点,这些设计主要是为了解决基本数据类型在面向对象编程中的局限性。

基本数据类型:

  1. 性能:基本数据类型(如int, double, char等)通常会有更好的性能,因为它们存储的是值,且存储在栈上。
  2. 存储:基本类型直接存储数据,它们不是对象,不具备对象的特性。
  3. 默认值:基本类型有默认值,例如int的默认值为0,boolean的默认值为false,无需显式初始化。
  4. 栈内存:基本类型的变量通常存储在栈内存上,这使得访问速度非常快。

封装类:

  1. 对象特性:封装类(如Integer, Double, Character等)是类,它们包装了基本类型的值并提供了对象的特性。
  2. 堆内存:封装类的实例存储在堆内存中,它们是引用类型。
  3. null值:封装类可以赋值为null,表示这个引用不指向任何对象,而基本数据类型不能赋值为null
  4. 工具方法:封装类提供了一些有用的工具方法,例如转换、比较、解析字符串为相应的类型等。
  5. 集合框架:Java的集合框架(如List, Set, Map等)只能存储对象,不能存储基本类型。这就是封装类存在的主要原因之一。
  6. 泛型:Java的泛型只支持引用类型,不支持基本数据类型。因此,需要使用封装类来使用泛型。
  7. 类型系统的一致性:将基本类型封装为类,让Java的类型系统更加统一,可以将基本类型当做对象处理。

设计原因:

  1. 性能与对象抽象:基本数据类型提供了性能优势,封装类提供了面向对象的抽象。这样的设计允许Java在保持较高性能的同时,又能利用面向对象的特性。
  2. 兼容集合框架:为了能在集合框架中使用基本类型的值,需要一个可以作为Object子类的类型,这就是封装类的作用。
  3. 泛型支持:泛型的引入使得类型安全的集合成为可能,封装类使得基本类型可以在这样的集合中使用。
  4. 自动装箱与拆箱:Java 5引入了自动装箱和拆箱,这使得基本类型和封装类之间的转换自动化,简化了编程。

封装类和基本数据类型之间的转换是自动的,这个特性被称为自动装箱和拆箱。例如,当你把一个int赋值给一个Integer对象时,自动装箱就发生了;反过来,当你把一个Integer赋值给一个int时,就会发生自动拆箱。这种设计让开发者能够在面向对象编程和性能之间找到平衡点。

使用基本数据类型场景:

  1. 性能敏感:如果你正在编写性能敏感的代码,比如在大规模数据处理、科学计算或者实时系统中,基本类型更快,因为它们避免了对象封装的开销。
  2. 默认值:当你需要确保类型总是有一个默认值,比如int默认是0,而不是null时。
  3. 数组处理:处理大量的数值数据时使用基本类型数组(如int[]),这样做可以减少内存消耗并提高处理速度。

使用封装类场景:

  1. 需要对象:当你需要将数字作为对象传递,例如在使用集合框架类ArrayListHashMap时,只能存储对象。
  2. 允许null:如果你需要表示一个变量可能没有值(即null),或者需要在某些业务逻辑中区分0null
  3. 泛型代码:Java的泛型不支持基本数据类型,所以在泛型代码中,例如ArrayList<Integer>,必须使用封装类。
  4. 使用对象方法:封装类提供了多种方法,比如将数字转换为字符串,或者与其他数字类型进行转换等。
  5. 系统编程或API调用:有些系统编程接口或API方法需要对象类型的参数,这时必须使用封装类。
  6. 序列化:只有对象才能被序列化,这样才能通过网络发送或写入文件等。

笔记-UTF-8、UTF-16编码

UTF-8编码方式:

UTF-8使用一至四个字节为每个字符编码,编码规则如下:

  1. 单字节的字符:对于标准的ASCII字符(U+0000至U+007F),UTF-8和ASCII编码是相同的,使用单个字节,其最高位始终为0。
  2. 多字节的字符:对于其他范围内的字符,则使用两个到四个字节表示:
    • 两字节字符:第一个字节的前三位为110,后面五位用于数据;第二个字节的前两位为10,后面六位用于数据。
    • 三字节字符:第一个字节的前四位为1110,后面四位用于数据;随后的字节的前两位为10,后面六位用于数据。
    • 四字节字符:第一个字节的前五位为11110,后面三位用于数据;随后的字节的前两位为10,后面六位用于数据。

每个字节中,以10开头的字节为后续字节,它们不会独立出现,只会跟随一个高位字节。这种编码方法的一个优势是通过查看任意字节,都能够知道它是单独的字符还是某个字符的一部分,因为UTF-8字节序列是顺序无关的,所以不像UTF-16和UTF-32,UTF-8不需要字节顺序标记(BOM)来指示字节的顺序

UTF-16编码方式:

UTF-16(16-bit Unicode Transformation Format)使用单个16位的代码单元来编码U+0000至U+FFFF之间的字符,这部分包括了基本的多语言平面(BMP)。对于更高位的字符,则使用一对16位的代码单元,即32位来编码,这种对称称为代理对(Surrogate Pair)。

  • BMP字符:使用一个16位的代码单元,直接对应于字符的Unicode码点。
  • 代理对:对于超出BMP的字符,使用两个16位的代码单元。第一个代码单元(高代理项)的范围是U+D800至U+DBFF,第二个代码单元(低代理项)的范围是U+DC00至U+DFFF。

UTF-8与UTF-16的区别:

  1. 字节顺序:UTF-8是字节顺序无关的,而UTF-16需要区分字节顺序,因此有UTF-16LE和UTF-16BE两种格式,分别代表小端序和大端序,有时还有带字节顺序标记(BOM)的格式。
  2. 空间效率:对于ASCII字符,UTF-8更为高效,只需要一个字节。而UTF-16不论什么字符至少需要两个字节。但对于很多亚洲语言,UTF-16可能更加空间高效,因为这些语言中的字符在UTF-8中可能需要三个或四个字节。
  3. 编码复杂性:UTF-8是可变长度的,从1到4个字节不等;UTF-16也是可变长度的,但主要是两个长度,要么是2个字节,要么是4个字节(代理对)。
  4. 兼容性:UTF-8编码兼容ASCII,因此被广泛用于网络和文件存储,特别是在主要交流用的是ASCII字符的系统中。UTF-16常用于需要处理大量非BMP字符的应用中,比如某些文本处理软件。