Java 基础
语言特性
优点
① 平台无关,摆脱硬件束缚,“一次编写,到处运行”。
② 安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。
③ 热点代码检测和运行时编译优化,程序随运行时长获得更高性能。
④ 完善的应用程序接口,支持第三方类库。
平台无关⭐
JVM: 编译器生成与计算机体系结构无关的字节码,字节码文件不仅能在任何机器解释执行,还能动态转换成本地机器码,转换由 JVM 实现。JVM 是平台相关的,屏蔽了不同操作系统的差异。
语言规范: 基本数据类型大小有明确规定,如 int 永远 32 位,而 C/C++ 可能是 16 位、32 位,或编译器开发商指定的其他大小。数值类型有固定字节数,字符串用标准 Unicode 格式。
JDK 和 JRE
JDK: Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE 及常用类库,是 JAVA 核心。
JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具。
值调用和引用调用
按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。
Java 总是按值调用,方法得到的是参数的副本,传递对象时实际上传递的是对象引用的副本。
-
方法不能修改基本数据类型的参数,例如传递了一个 int 值 ,改变 int 值不会影响实参。
-
方法可以改变对象参数的状态,但不能让对象参数引用新的对象。例如传递了一个 int 数组,改变数组内容会影响实参,而改变其引用并不会让实参引用新的数组对象。
浅拷贝和深拷贝
浅拷贝只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象。
深拷贝完全拷贝基本数据类型和引用数据类型,修改克隆对象不会影响原对象。
反射
在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射,缺点是破坏了封装性及泛型约束。
Class 类
在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
获取 Class 对象:① 类名.class
。② 对象的 getClass
方法。③ Class.forName(类的全限定名)
。
注解⭐
注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override
标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target
:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。
@Rentention
:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。
@Documented
:表明注解应该被 javadoc 记录。
泛型
泛型本质是参数化类型,解决不确定对象具体类型的问题。
泛型的好处:① 类型安全,不存在 ClassCastException。② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的数据类型。
泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 List<Object>
或 List<String>
,在编译后都会变成 List
。
JDK8 新特性
**lambda 表达式:**允许把函数作为参数传递到方法,简化匿名内部类代码。
**函数式接口:**使用 @FunctionalInterface
标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
**方法引用:**可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
**接口:**接口可以定义 default
修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
**注解:**引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
**类型推测:**加强了类型推测机制,使代码更加简洁。
**Optional 类:**处理空指针异常,提高代码可读性。
**Stream 类:**引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach
遍历、count
统计个数、filter
按条件过滤、limit
取前 n 个元素、skip
跳过前 n 个元素、map
映射加工、concat
合并 stream 流等。
**日期:**增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
**JavaScript:**提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。
异常🌙
所有异常都是 Throwable 的子类,分为 Error 和 Exception。
Error 是 Java 运行时系统的内部错误和资源耗尽错误,例如 StackOverFlowError 和 OutOfMemoryError,这种异常程序无法处理。
Exception 分为受检异常和非受检异常,受检异常要显式处理,否则编译出错,非受检异常是运行时异常,继承 RuntimeException。
受检异常:① 无能为力型,如字段超长导致的 SQLException。② 力所能及型,如未授权异常 UnAuthorizedException,程序可跳转权限申请页面。常见受检异常还有 FileNotFoundException、ClassNotFoundException、IOException等。
非受检异常:① 可预测异常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提前处理。② 需捕捉异常,例如进行 RPC 调用时的远程服务超时,这类异常客户端必须显式处理。③ 可透出异常,指框架或系统产生的且会自行处理的异常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 会自动将异常自动映射到合适的状态码。
数据类型
基本数据类型
数据类型 | 内存大小 | 默认值 | 取值范围 |
---|---|---|---|
byte | 1 B | (byte)0 | -128 ~ 127 |
short | 2 B | (short)0 | -215 ~ 215-1 |
int | 4 B | 0 | -231 ~ 231-1 |
long | 8 B | 0L | -263 ~ 263-1 |
float | 4 B | 0.0F | ±3.4E+38(有效位数 6~7 位) |
double | 8 B | 0.0D | ±1.7E+308(有效位数 15 位) |
char | 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 | ‘\u0000’ | ‘\u0000’ ~ ‘\uFFFF’ |
boolean | 单个变量 4B / 数组 1B | false | true、false |
JVM 没有 boolean 的字节码指令,单个 boolean 变量用 int 代替,boolean f = false
就是用 ICONST_0 即常数 0 赋值。boolean 数组会编码成 byte 数组。
自动装箱是将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素;自动拆箱是将一个包装类对象转换为基本数据类型,例如将一个 Integer 对象赋值给一个 int 变量。比较两个包装类数值要用 equals
。
String⭐
String 类和其存储数据的 value 字节数组都是 final 修饰的。对 String 对象的任何修改实际都是创建新对象再引用,并没有修改原对象。
字符串拼接方式
① 直接用 +
,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 +
拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat
方法,该方法使用 Arrays.copyOf
创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf,之后调用 getChars
方法用 System.arraycopy
将拼接字符串的值也拷贝到 buf,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +
。
③ 使用 StringBuilder 或 StringBuffer,两者的 append
方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf
确定新的字符数组容量,再调用 getChars
方法用 System.arraycopy
将新的值追加到数组。StringBuilder 是 JDK5 引入的,效率高但线程不安全,StringBuffer 使用 synchronized 保证线程安全。
面向对象
面向对象
面向过程是过程化思维,代码松散,强调流程化,开发时软件维护困难,耦合严重;面向对象更适合解决大规模问题,强调高内聚、低耦合,先抽象模型定义共性行为,再解决问题。
封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等级。主要任务是对属性、数据、敏感行为实现隐藏,使对象关系变得简单,降低耦合。
继承用来扩展类,子类可继承父类的部分属性和行为,使模块具有复用性。
多态以封装和继承为基础,根据运行时对象实际类型使同一行为具有不同表现形式。多态指在编译层面无法确定最终调用的方法体,在运行期由 JVM 动态绑定,调用合适的重写方法。由于重载属于静态绑定,本质上重载结果是完全不同的方法,因此多态一般专指重写。
重载和重写
重载指方法名称相同,但参数列表不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法。不管继承关系多复杂,重载在编译时可以确定调用哪个方法,因此属于静态绑定。重载顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围。③ 自动拆箱与装箱。④ 子类向上转型。⑤ 可变参数。
重写指子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。元空间有一个方法表保存方法信息,如果子类重写父类的方法,方法表中的方法引用会指向子类。重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大。
Object 类⭐
方法 | 说明 |
---|---|
equals | 检测对象是否相等,默认使用 == 比较,可以重写该方法自定义规则。规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。 |
hashCode | 每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同。 |
toString | 默认打印表示对象值的一个字符串。 |
clone | 默认声明为 protected,只能由本类对象调用,且是浅拷贝。一般重写 clone 方法需要实现 Cloneable 接口并声明为 public,如果没有实现 Cloneable 接口会抛出 CloneNotSupport 异常。 |
finalize | GC 判断垃圾时,如果对象没有与 GC Roots 相连会被第一次标记,之后判断对象是否有必要执行 finalize 方法,有必要则由一条低调度优先级的 Finalizer 线程执行。虚拟机会触发该方法但不保证结束,防止方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链相连,就会在第二次标记时移出回收集合。由于运行代价高且具有不确定性,在 JDK9 标记为过时方法。 |
getClass | 返回对象所属类的 Class 对象。 |
wait | 阻塞持有该对象锁的线程。 |
notify | 唤醒持有该对象锁的线程,notify 随机唤醒一个线程,notifyAll 唤醒全部线程。 |
内部类
内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。
静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名
直接访问,只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类都是静态内部类。
成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。
访问权限控制符
访问权限控制符 | 本类 | 包内 | 包外子类 | 任何地方 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
无 | √ | √ | × | × |
private | √ | × | × | × |
接口和抽象类
接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。
语法维度 | 抽象类 | 接口 |
---|---|---|
成员变量 | 无特殊要求 | 默认 public static final 常量 |
构造方法 | 有构造方法,不能实例化 | 没有构造方法,不能实例化 |
方法 | 抽象类可以没有抽象方法 | 默认 public abstract,JDK8 支持默认/静态方法,JDK9 支持私有方法。 |
继承 | 单继承 | 多继承 |
抽象类是 is-a 关系,接口是 can-do 关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。
抽象类是模板式设计,包含一组具体特征,例如汽车的底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅可以根据不同级别配置存在不同实现。
接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守约定。例如所有车辆都必须实现刹车这种强制规范。
接口是顶级类,抽象类在接口下面的第二层,对接口进行组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现,方便扩展和重构。
集合
ArrayList⭐
ArrayList 是容量可变列表,使用数组实现,扩容时会创建更大的数组,把原有数组复制到新数组。支持对元素的随机访问,但插入与删除速度慢。ArrayList 实现了 RandomAcess 接口,如果类实现了该接口,使用索引遍历比迭代器更快。
elementData 是 ArrayList 的数据域,被 transient 修饰,序列化时调用 writeObject
写入流,反序列化时调用 readObject
重新赋值到新对象的 elementData。原因是 elementData 容量通常大于实际存储元素的数量,所以只需发送真正有值的元素。
size 是当前实际大小,小于等于 elementData 的大小。
modCount 记录了 ArrayList 结构性变化的次数,继承自 AbstractList。expectedModCount 是迭代器初始化时记录的 modCount 值,每次访问新元素时都会检查 modCount 是否等于 expectedModCount,不等将抛出异常。这种机制叫 fail-fast,所有集合类都有。
LinkedList⭐
LinkedList 本质是双向链表,与 ArrayList 相比增删速度更快,但随机访问慢。除继承 AbstractList 外还实现了 Deque 接口,该接口具有队列和栈的性质。成员变量被 transient 修饰,原理和 ArrayList 类似。
包含三个重要的成员:size、first 和 last。size 是双向链表中节点的个数,first 和 last 分别指向首尾节点。
优点:可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率高。
Set
Set 元素不重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet。
HashSet 通过 HashMap 实现,HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性。HashSet 判断元素是否相同时,对于包装类型直接按值比较,对于引用类型先比较 hashCode,不同则代表不是同一个对象,相同则比较 equals,都相同才是同一个对象。
LinkedHashSet 继承自 HashSet,通过 LinkedHashMap 实现,使用双向链表维护元素插入顺序。
TreeSet 通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
TreeMap⭐
TreeMap 基于红黑树实现,增删改查的平均和最差时间复杂度均为 O(logn) ,最大特点是 Key 有序。Key 必须实现 Comparable 接口或 Comparator 接口,所以 Key 不允许为 null。
TreeMap 依靠 Comparable 或 Comparator 排序,如果实现了 Comparator 就会优先使用 compare
方法,否则使用 Comparable 的 compareTo
方法,两者都不满足会抛出异常。
TreeMap 通过 put
和 deleteEntry
实现增加和删除树节点。插入新节点的规则有三个:① 需要调整的新节点总是红色的。② 如果插入新节点的父节点是黑色的,不需要调整。③ 如果插入新节点的父节点是红色的,由于红黑树不能出现相邻红色,进入循环判断,通过重新着色或左右旋转来调整。
HashMap ⭐
JDK8 前底层使用数组加链表,JDK8 改为数组加链表/红黑树,节点从 Entry 变为 Node。主要成员变量包括 table 数组、元素数量 size、加载因子 loadFactor。
table 数组记录 HashMap 的数据,每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表,Node/Entry 节点包含四个成员变量:key、value、next 和 hash。
数据以键值对的形式存在,键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样,就会发生哈希冲突,被放到同一个链表上,为使查询效率尽可能高,键的 hash 值要尽可能分散。
默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。
JDK8 之前
hash:计算元素 key 的散列值
① 处理 String 类型时,调用 stringHash32
方法获取 hash 值。
② 处理其他类型数据时,提供一个随机值 hashSeed 作为计算初始量,执行异或和无符号右移使 hash 值更加离散。
indexFor:计算元素下标
将 hash 值和数组长度-1 进行与操作,保证结果不超过 table 范围。
get:获取元素的 value 值
key 为 null,调用 getForNullKey
方法:
- size=0 表示链表为空,返回 null。
- size!=0 说明存在链表,遍历 table[0] 链表,如果找到了 key=null 的节点则返回其 value,否则返回 null。
key 不为 null,调用 getEntry
方法:
- size=0 表示链表为空,返回 null 值。
- size!=0,首先计算 key 的 hash 值,然后遍历该链表的所有节点,如果节点的 key 和 hash 值都和要查找的元素相同则返回其 Entry 节点。 如果找到了对应的 Entry 节点,调用
getValue
方法获取其 value 并返回,否则返回 null。
put:添加元素
key 为 null,直接存入 table[0]。
key 不为 null,计算 key 的 hash 值,调用 indexFor
计算元素下标 i,遍历 table[i] 链表:
- key 已存在,更新 value 然后返回旧 value。
- key 不存在,将 modCount 加 1,调用
addEntry
方法增加一个节点并返回 null。
resize:扩容数组
当前容量达到了最大容量,将阈值设置为 Integer 最大值,之后扩容不再触发。
当前容量没达到最大容量,计算新的容量,将阈值设为 newCapacity x loadFactor
和 最大容量 + 1
的较小值。创建一个容量为 newCapacity 的 Entry 数组,调用 transfer
方法将旧数组的元素转移到新数组。
transfer:转移元素
遍历旧数组的所有元素,调用 rehash
方法判断是否需要哈希重构,如果需要就重新计算元素 key 的 hash 值。
调用 indexFor
方法计算元素存放的下标 i,利用头插法将旧数组的元素转移到新数组。
JDK8
hash:计算元素 key 的散列值
如果 key 为 null 返回 0,否则就将 key 的 hashCode
方法返回值高低16位异或,让尽可能多的位参与运算,让结果的 0 和 1 分布更加均匀,降低哈希冲突概率。
put:添加元素
调用 putVal
方法添加元素:
- 如果 table 为空或不存在元素就进行扩容,否则计算元素下标位置,不存在就调用
newNode
创建一个节点。 - 如果存在元素且是链表类型,如果首节点和待插入元素相同,直接更新节点 value。
- 如果首节点是 TreeNode 类型,调用
putTreeVal
方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert
方法,插入节点并调整平衡、moveRootToFront
方法,由于调整平衡后根节点可能变化,需要重置根节点。 - 如果都不满足,遍历链表,根据 hash 和 key 判断是否重复,决定更新 value 还是新增节点。如果遍历到了链表末尾则添加节点,如果达到建树阈值 7,还需要调用
treeifyBin
把链表重构为红黑树。 - 存放元素后将 modCount 加 1,如果
++size > threshold
,调用resize
扩容。
get :获取元素的 value 值
调用 getNode
方法获取 Node 节点:
-
如果数组不为空且存在元素,先比较第一个节点和要查找元素,如果相同则直接返回。
-
如果第二个节点是 TreeNode 类型则调用
getTreeNode
方法进行查找。 -
都不满足,遍历链表根据 hash 和 key 查找,如果没有找到就返回 null。
-
如果节点不是 null 就返回其 value,否则返回 null。
resize:扩容数组
重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。
重新规划长度
① 如果当前容量 oldCap > 0
且达到最大容量,将阈值设为 Integer 最大值,终止扩容。
② 如果未达到最大容量,当 oldCap << 1
不超过最大容量就扩大为 2 倍。
③ 如果都不满足且当前扩容阈值 oldThr > 0
,使用当前扩容阈值作为新容量。
④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12。
重新排列数据节点
① 如果节点为 null 不进行处理。
② 如果节点不为 null 且没有 next 节点,通过节点的 hash 值和 新容量-1
进行与运算计算下标存入新的 table 数组。
③ 如果节点为 TreeNode 类型,调用 split
方法处理,如果节点数 hc 达到 6 会调用 untreeify
方法转回链表。
④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0
的部分不需要做处理,否则需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。
线程不安全
JDK7 存在死循环和数据丢失问题。
数据丢失:
-
并发赋值被覆盖: 在
createEntry
方法中,新添加的元素放在头部,使元素可以被更快访问,但如果两个线程同时执行到此处,会导致数据覆盖。 -
新表被覆盖: 如果多线程同时
resize
,每个线程都会 new 一个数组,这是线程内的局部对象,线程间不可见。迁移完成后resize
的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。
死循环: 扩容时 resize
调用 transfer
使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改,某线程还没有将 table 设为 newTable 时用完了 CPU 时间片。
JDK8 在 resize
方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据。可用 ConcurrentHashMap 或 Collections.synchronizedMap
包装同步集合。
IO 流
BIO
BIO 是同步阻塞式 IO,JDK1.4 前的 IO 模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要开销。可以通过线程池改善,称为伪异步 IO。适用连接数目少且服务器资源多的场景。
NIO
NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。
同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。
核心组件:
-
Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。
-
Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。
-
Buffer: 缓冲区,是一块可读写数据的内存。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。
flip
将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。clear
将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。compact
将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。- 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。
使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。
AIO
AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。
异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
实现方式包括通过 Future 的 get
方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed
和请求失败回调方法 failed
。
java.io
主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。
字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。
序列化
Java 对象在 JVM 退出时会全部销毁,如果需要将对象持久化就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。
常见的序列化有三种:
-
Java 原生序列化
实现
Serializabale
标记接口,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化 ID 的一致,一般使用
private static final long serialVersionUID
定义序列化 ID,如果不设置编译器会根据类的内部实现自动生成该值。 -
Hessian 序列化
支持动态类型、跨语言,对象序列化的二进制流可以被其它语言反序列化。特性:① 自描述序列化类型,不依赖外部描述文件。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。
-
JSON 序列化
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化通常使用网络传输对象,容易遭受攻击,Jackson 和 fastjson 都出现过反序列化漏洞,因此不需要进行序列化的敏感属性应加上 transient 关键字。transient 的作用是把变量生命周期仅限于内存,不会写到磁盘,变量会被设为对应数据类型的零值。