文章

《Java 基础知识总结》

《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 中一个常见的坑点

  1. 两个包装类型比较用 equals(不用考虑 128)
  2. 基本类型与包装类型混用:例如用 == 比较,包装类型会自动拆箱
  3. 两个包装类型用 == 做比较(考虑 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

== 运算符

  1. 比较基本类型:直接比较值是否相等(常用)
  2. 比较引用类型:比较的是地址(考虑 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)

枚举的用途

用来定义一组指定域的常量值(如:星期一、星期二……)

枚举的特点

  1. 每一个常量值都是 public static final 默认修饰
  2. 在类加载时,通过静态代码块来初始化
  3. 方便编译器自动检测要比较的值是否在指定域中(因为 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 线程负责监听客户端的连接。

工作流程:

  1. 在 while(true) 循环中服务端调用 accept() 方法等待接收客户端的连接
  2. 请求一旦接收到一个连接请求,就可以建立通信套接字
  3. 在这个通信套接字上进行读写操作
  4. 此时不能再接收其他客户端连接请求
  5. 只能等待同当前连接的客户端的操作执行完成
  6. 处理完成之后,通过输出流返回应答给客户端,线程销毁

可以通过多线程来支持多个客户端的连接

优点:

  • 模型简单
  • 可以让每一个连接专注于自己的 I/O
  • 编程模型简单,不用过多考虑系统的过载、限流等问题

缺点:

  • 线程的资源非常宝贵
  • 频繁的创建和销毁线程非常消耗资源
  • 上下文切换成本也很高
2. 伪异步 IO

主要是来解决频繁创建线程问题。

改进措施:

  • 加了一个线程池和一个任务队列
  • 避免频繁的创建和销毁线程
  • 把客户端发过来的 Socket 请求封装成一个 Task 线程
  • 投递到 JDK 的线程池里面去处理
  • JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理

好处:

  • 解决频繁创建线程问题
  • JDK 线程池线程可控,不会出现系统宕机等问题

缺点:

  • 底层仍然是同步阻塞的 BIO 模型
  • 无法从根本上解决线程浪费的问题

NIO (New I/O)

简介

同步非阻塞的 I/O 模型,不再面向流,面向缓冲区,基于通道的一种 I/O 模型。

三大核心组件

  1. 缓冲区 (Buffer)
  2. 通道 (Channel)
  3. 选择器 (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 目前的应用不是很广泛

📚 参考资料:本文内容基于个人学习笔记整理

本文由作者按照 CC BY 4.0 进行授权