《Java 基础知识总结》
一、基本数据类型
Java 中的基本数据类型分为四大类:
数据类型分类
- 整数类型:
byte、short、int、long - 浮点数类型:
float、double - 字符类型:
char(Unicode 编码中英文单个字符都占 2 个字节) - 布尔类型:
boolean(占 4 个字节,在布尔数值中占 1 个字节)
包装类型
每个基本类型都有对应的包装类型,转换由编译器自动完成:
- 自动装箱:基本类型转包装类型
1
Integer total = 99; // 等价于 Integer total = Integer.valueOf(99)
- 自动拆箱:包装类型转基本类型
1
int j = total; // 等价于 int j = total.intValue()
类型转换
- 低字节转高字节:可以直接转换
int→float、long→double:会有精度损失- 高字节转低字节:需要强制转换
128 陷阱
⚠️ 这是 Java 中一个常见的坑点
- 两个包装类型比较用
equals(不用考虑 128) - 基本类型与包装类型混用:例如用
==比较,包装类型会自动拆箱 - 两个包装类型用
==做比较(考虑 128):数值超过 128 会重新创建对象
常量池机制:
Java 基本类型的包装类大部分实现了常量池技术(JDK 8 及以前):
Byte、Short、Integer、Long、Character、Boolean这 6 种包装类默认创建了数值 [-128, 127] 的缓存数据- 超过这个范围仍然会去创建新的对象
Float、Double没有实现常量池技术,会有计算精度问题Integer包装类的 128 上限可以自定义(原因:int 用的比较多,节约内存)
二、成员变量和局部变量的区别
| 特性 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 在类中,方法外定义 | 在方法中定义 |
| 作用域 | 可以被整个类中的方法或代码块使用 | 只针对特定范围有效 |
| 生命周期 | 随着对象的创建而创建,随着对象的销毁而销毁 | 当方法执行完,或语句结束后,就会被销毁 |
| 初始值 | 有默认初始值 | 没有初始值,需要先赋值再使用 |
| 存储位置 | 存储在方法区 | 存储在栈中(不会被初始化) |
📌 注意:静态变量(全局和局部静态变量)也存储在方法区
三、常量
final 关键字的用途
- 修饰常量:常量名大写,不可修改
1
final int MAX_VALUE = 100;
- 修饰方法:不能被重写
1
public final void calculate() { }
- 修饰类:不能被继承
1
public final class FinalClass { }
四、运算符
特殊运算符
- 右结合运算符:
=、+=、-=等赋值运算符;正负符号+-;取反符号!;++-- - 位运算符:用于位级操作
== 与 equals 的区别
equals 方法
- 主要用来比较两个引用类型的内容(常用)
Object超类默认比较的是两个地址,所以要比较两个对象的内容,需要重写 equals 方法String、Integer、Date等都重写了equals
== 运算符
- 比较基本类型:直接比较值是否相等(常用)
- 比较引用类型:比较的是地址(考虑 128 陷阱)
五、重写了 equals 为什么还要重写 hashCode?
两个重要原因
1. 提高效率
使用 hashCode() 方法提前校验,可以避免每一次比对都调用 equals() 方法:
- 如果两个对象
hashCode不一样,那它们必然不是同一个对象 - 如果两个对象
hashCode一样,它们可能相等,再去equals()比较内容,确保两个对象相等
2. 保证是同一个对象
如果重写了 equals 方法,而没有重写 hashCode 方法,会出现:
equals相等的对象,hashCode不相等的情况
这违反了 hashCode 的规则:
两个对象相等,其 hashCode 一定相等
重写前后的区别:
- 不重写 hashCode:计算规则是根据对象在内存中的地址散列得来
- 重写 hashCode:根据对象的内容来散列计算
六、字符串
String 的特性
- 引用类型,本身也是一个
class - 底层是字符数组
char[](较新的 JDK 版本底层是byte[]) - 字符串的不变性:因为底层是
private final char[],且没有任何修改字符数组的方法 - 比较两个字符串的内容用
equals,不能用== - 字符串操作不改变原字符串内容,而是返回新字符串
- Java 使用 Unicode 编码表示
String和char
String / StringBuffer / StringBuilder 的区别
为什么要引入其它两种字符串处理类?
核心原因:提高字符串连接效率
在大部分情况下,字符串的拼接速度:StringBuilder > StringBuffer > String
String
- 不可变的,因此每次对其操作改变其变量值,其实是生成一个新的对象,然后将变量引用指向新对象
- 速度慢
- 线程安全
StringBuffer
- 对其操作即直接操作对象指向的引用,无需产生新对象
- 速度快
- 线程安全(维护多线程同步会消耗一点性能)
StringBuilder
- JDK 5 之后新增,用法与
StringBuffer完全一致 - 线程不安全
- 在单线程中是最快的(不需要维护线程安全)
String.intern() 方法
String.intern() 查找常量池中是否有相同 Unicode 的字符串常量:
- 如果有,则返回其引用
- 如果没有,则在常量池中增加一个 Unicode 等于 str 的字符串并返回它的引用
字符串存放位置
- String 字面量:存放在常量池,在编译期已经被确定了
- new String():不是字符串常量,它有自己的地址空间,存放在堆空间
- 字符串拼接操作:相当于 new 了一个 String 对象,存放在堆中
七、枚举类型(enum)
枚举的用途
用来定义一组指定域的常量值(如:星期一、星期二……)
枚举的特点
- 每一个常量值都是
public static final默认修饰 - 在类加载时,通过静态代码块来初始化
- 方便编译器自动检测要比较的值是否在指定域中(因为 enum 常量自带类型信息)
引用类型特性
enum是引用类型,Java 引用类型的数据通常用equals比较- 但
enum可以用==比较 - 原因:enum 中的每一个常量在 JVM 中只有唯一的实例(天然的单例)
底层实现
1
2
3
4
// enum 底层就是
final class Xxx extends Enum {
// 类的具体实现
}
特点:
- 不能被继承
- 不能 new
- 只能在类当中定义常量
- 只能重写父类的
toString方法,其他方法都被final修饰
八、Java 泛型
泛型的作用
Java 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
九、I/O、BIO、NIO、AIO
后三者为封装的 I/O 接口
I/O
1. IO 流的分类
- 按照流的流向分:输入流和输出流
- 按照操作单元划分:字节流和字符流
- 按照流的角色划分:节点流和处理流
2. 流的原理
Java IO 流共涉及 40 多个类,这些类都是从如下 4 个抽象基类中派生出来的:
InputStream/Reader:所有输入流的基类- 前者是字节输入流
- 后者是字符输入流
OutputStream/Writer:所有输出流的基类- 前者是字节输出流
- 后者是字符输出流
BIO (Blocking I/O)
简介
同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。
分类
1. 传统 BIO(一请求一应答)
采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。
工作流程:
- 在
while(true)循环中服务端调用accept()方法等待接收客户端的连接 - 请求一旦接收到一个连接请求,就可以建立通信套接字
- 在这个通信套接字上进行读写操作
- 此时不能再接收其他客户端连接请求
- 只能等待同当前连接的客户端的操作执行完成
- 处理完成之后,通过输出流返回应答给客户端,线程销毁
可以通过多线程来支持多个客户端的连接
优点:
- 模型简单
- 可以让每一个连接专注于自己的 I/O
- 编程模型简单,不用过多考虑系统的过载、限流等问题
缺点:
- 线程的资源非常宝贵
- 频繁的创建和销毁线程非常消耗资源
- 上下文切换成本也很高
2. 伪异步 IO
主要是来解决频繁创建线程问题。
改进措施:
- 加了一个线程池和一个任务队列
- 避免频繁的创建和销毁线程
- 把客户端发过来的 Socket 请求封装成一个 Task 线程
- 投递到 JDK 的线程池里面去处理
- JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理
好处:
- 解决频繁创建线程问题
- JDK 线程池线程可控,不会出现系统宕机等问题
缺点:
- 底层仍然是同步阻塞的 BIO 模型
- 无法从根本上解决线程浪费的问题
NIO (New I/O)
简介
同步非阻塞的 I/O 模型,不再面向流,面向缓冲区,基于通道的一种 I/O 模型。
三大核心组件
- 缓冲区 (Buffer)
- 通道 (Channel)
- 选择器 (Selector)
NIO 与传统 I/O 区别
| 特性 | NIO | 传统 I/O |
|---|---|---|
| 面向对象 | 面向缓冲区 | 面向流 |
| 传输方向 | 管道传输是双向的 | 流是单向的 |
| 阻塞性 | 非阻塞的 | 阻塞的 |
非阻塞特性
单线程中:
- 从通道读取数据到 buffer(不需要等数据全读完)
- 同时可以继续做别的事情
- 当数据读取到 buffer 中后,线程再继续处理数据
- 写数据也是一样的
缓冲区(Buffer)
- 最常用的缓冲区是
ByteBuffer - 缓冲区其实是一个对象,它包含一些要写入或者要读出的数据
- 每一种 Java 基本类型(除了 Boolean 类型)都对应有一种缓冲区
- 缓冲的好处:可以把数据转成 byte 等类型,可以压缩数据
通道(Channel)
- NIO 通过 Channel(通道)进行读写
- 通道是双向的,可读也可写
- 通道只能和 Buffer 交互
- 因为 Buffer,通道可以异步地读写
选择器(Selector)
- 选择器用于使用单个线程处理多个通道
- 它需要较少的线程来处理这些通道
- 线程之间的切换对于操作系统来说是昂贵的
- 为了提高系统效率,选择器是有用的
优点:
- 双向传输数据
- 非阻塞与选择器的加入避免线程资源浪费
缺点:
- 传统的 JDK 原生 NIO 编程实现有空轮询 bug 导致 CPU 资源飙升
- JDK 原生 NIO 编程复杂,项目过大时可能出现 bug
- Netty 的出现很大程度上解决了这些问题
AIO (Asynchronous I/O)
简介
AIO 也就是 NIO 2,NIO 的改进版。它是异步非阻塞的 I/O 模型。
异步 IO 的特点
- 基于事件和回调机制实现
- 应用操作之后会直接返回,不会堵塞在那里
- 当后台处理完成,操作系统会通知相应的线程进行后续的操作
NIO 与 AIO 的区别
虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的:
- 对于 NIO 来说,业务线程是在 IO 操作准备好时,得到通知
- 接着就由这个线程自行进行 IO 操作
- IO 操作本身是同步的(除了 AIO,其他的 IO 类型都是同步的)
优点:
- 非同步,用户体验良好
缺点:
- AIO 目前的应用不是很广泛
📚 参考资料:本文内容基于个人学习笔记整理