Java朝花夕拾
基础
[toc]
基本语法
除非确实要处理 UTF-16 代码单元,否则不要在程序中使用
char
类型strictfp
关键字修饰的方法中,所有的指令都必须使用严格的浮点计算默认情况下,虚拟机设计者允许对中间计算结果采用扩展的精度(扩展的指数),即浮点运算结果不可预知。
有时我们可能想要得到完全可预测的浮点计算,即浮点计算的结果在各处理器平台都一样,那么就可以使用
strictfp
关键字。但是由于该关键字会对中间结果进行截断操作,而截断操作需要消耗时间,所以在计算速度上比精确计算要慢。因此,在Java中,最优性能与理想结果之间存在冲突。strictfp
关键字可用来修饰类、接口或方法。使用strictfp
关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。严格的浮点计算表示浮点计算完全依照浮点规范 IEEE-754 来执行。需要注意的是,采用严格浮点计算可能会产生溢出,而默认情况下,不会产生溢出。对大多数程序来说, 浮点溢出不属于大向题。当用二元运算符连接两个值时,先要进行类型转换。优先级
double > float > long > int
1
2
3
4
5
6
7
8
9
10int a = 1;
double b = 2.1;
long c = 1L;
float d = 3.2F;
char e = 'a';
byte f = 10;
double res1 = a + b; // double
float res2 = c + d; // float
int res3 = e + f; // int
long res4 = c + e; // long1、两操作数中有一个是
double
,另一个会被转换为double
计算;2、否则若其中有一个是
float
,另一个会被转换为float
计算;3、否则若其中有一个是
long
,另一个会被转换为long
计算;4、否则两个操作数都将被转为
int
计算。二元运算符(+=, -= 之类)。如果运算符得到一个值,其类型与左侧操作数类型不同,则会发生强转。
1
2int x = 1;
x += 1.5; // 等价于 x = (int)(x + 1.5)条件与或运算注意 短路 现象。
**&& **运算符检查第一个表达式是否返回
false
,如果是false
则直接返回false
,不再执行右侧的语句。|| 运算符检查第一个表达式是否返回
true
,如果是true
则直接返回true
,不再执行右侧的语句。1
2
3
4// 当 x 为 0 时,不会执行 1 / x 的检查
if (x != 0 && 1 / x > 1) {
// TODO
}Java 的字符串类似于 C++ 中的 char* 指针
1
char* s = "hello";
StringBuffer
效率略低于StringBuilder
。StringBuffer
支持多线程。两个类的 API 一样。
for
语句的三个部分应该对同一个计数器变量进行初始化、检测和更新。由于
switch
语句存在 “直通式”(fall through)行为,程序中不应使用switch
语句。switch
中本质上只能使用整型,任何类型的比较都要转换成整型。switch
中使用String
作为条件,会由编译器转化为hashCode()
进行比较。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
---->
public class switchDemoString {
public switchDemoString() {
}
public static void main(String args[]) {
String str = "world";
String s;
switch((s = str).hashCode()) {
default:
break;
case 99162322:
// 额外使用 equals(),防止哈希碰撞
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}switch
的底层为 tableswitch(适用于 case 范围小,查找方式类似数组的 index )和 lookupswitch(适用于 case 范围大,表中的 case 按序排列,查找方式为二分查找)两种表格带标签的
break
,可用于程序跳转。多用于跳出多重循环。标签必须放在希望跳出的最外层循环之前。1
2
3
4
5
6
7
8labelOne:
for (...) {
for (...) {
if (...) {
break labelOne;
}
}
}数组长度不要求为常量
数组的简明形式初始化为大括号
1
2
3int n = 2;
int a1 = new int[n]; // 创建一个长度为 n 的数组
int[] a2 = {1, 2};Arrays.sort()
方法使用的是快排Java 10 之后,引入了关键字
var
,用于方法中的局部变量,从变量的初始值推导出它们的类型。1
var p1 = new Person("zhangsan");
参数列表中的省略号 … 表示可以接收任意数量的对象
1
2
3
4
5
6public static void main(String[] args) {
// TODO
}
public static void main(String... args) {
// TODO
}String.intern()
是一个 native 方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况: 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
1
2
3
4
5
6
7String s1 = "Java";
String s2 = s1.intern();
String s3 = new String("Java");
String s4 = s3.intern();
System.out.println(s1 == s2); // true
System.out.println(s3 == s4); // false
System.out.println(s1 == s4); //true被
final
关键字修改之后的String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。如果编译器在运行时才能知道其确切值的话,就无法对其优化1
2
3
4
5
6
7
8
9
10
11
12
13final String str1 = "str";
final String str2 = "ing";
final String str3 = getStr();
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
String e = str1 + str3; // 在堆上创建的新的对象
System.out.println(c == d);// true
System.out.println(c == e);// false
public static String getStr() {
return "ing";
}
对象与类
若要返回一个可变数据字段的副本,应该使用
clone()
,而不是直接返回1
2
3
4
5
6class Employee{
...
public Date getHireDay() {
return (Date) hireDay.clone();
}
}
类设计技巧:
一定要保证数据私有
一定要对数据进行初始化
不要在类中使用过多的基本类型
1
2
3
4
5private String firstName;
private String lastName;
private int age;
--->
private Person p1;并非所有字段都需要单独的字段访问器和字段更改器
分解有过多职责的类
类名和方法名要体现职责
优先使用不可变的类
this 关键字与 super 关键字
相同:都具有两个含义。
this
:1、指示隐式参数的引用;2、调用该类的其他构造器。super
:1、调用超类的方法;2、调用超类的构造器。
不同:this 是对当前对象的引用, 而 super 只是一个只是编译器调用超类方法的特殊关键字,并不是一个对象的引用。
由于子类的构造器默认会调用超类的无参构造器,因此若超类无无参构造器,且子类的构造器中未显式调用超类的其他构造器,编译器会报错。
虚拟机预先为每个类计算量一个方法表(method table),列出了所有方法的签名(方法名和参数类型)和调用的实际方法。
调用某个方法的解析过程如下:
- 虚拟机获取对象的实际类型的方法表;
- 虚拟机查找定义了该方法签名的类,此时虚拟机已经知道应该调用哪个方法;
- 虚拟机调用。
在继承链上进行从上至下的强制类型转换,并谎报对象的内容,则在程序运行时,会抛出一个
ClassCastException
异常。1
Manager boss = (Manager) staff[0]; // Error!
因此要养成习惯:进行强制类型转换前,使用
instanceof
检查类型1
2
3
4if (staff[0] instanceof Manager) {
boss = (Manager) staff[0];
// TODO
}装箱过程是调用包装器的
valueOf()
方法,拆箱过程是调用包装器的xxxValue
方法String
字符串内容相同,则其散列码也相同,因为其重写了hashCode()
方法,哈希值由字符串内容决定。StringBuilder
字符串内容相同,散列码不一定相同,因为其未定义hashCode()
方法,而Object
类的默认hashCode()
方法由对象的存储地址得出。1
2
3
4
5
6String s = "hi";
String t = "hi";
System.out.println(s.hashCode() == t.hashCode()); // true
StringBuilder sb1 = new StringBuilder("hi");
StringBuilder sb2 = new StringBuilder("hi");
System.out.println(sb1.hashCode() == sb2.hashCode()); // false一般的类都会重写
toString()
方法以友好的方式打印对象,令人烦恼的是,数组没有重写,而是继承了Object
类的toString()
。调用
Arrays.toString()
打印数组,调用Arrays.deepToString()
打印多维数组。1
2
3
4
5int[] a = {1, 2};
int[][] b = {{1, 2}, {3}};
System.out.println(a); // [I@7852e922
System.out.println(Arrays.toString(a)); // [1, 2]
System.out.println(Arrays.deepToString(b)); // [[1, 2], [3]]使用
ArrayList
实现运行时动态更改数组大小,ArrayList
类似数组,但在添加或删除元素时,自动调整数组容量。1
2ArrayList<Employee> staff = new ArrayList<>();
staff.add(new Employee("Zhangsan", 18));枚举类:
1
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE}
实质上是定义了一个类,刚好有 4 个实例对象,且不可能构造新的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public final class Size extends Enum
{
private Size(String s, int i)
{
super(s, i);
}
// ...
static
{
SMALL = new T("SMALL", 0);
MEDIUM = new T("MEDIUM", 1);
LARGE = new T("LARGE", 2);
EXTRA_LARGE = new T(" EXTRA_LARGE", 3);
ENUM$VALUES = (new T[] {
SMALL, MEDIUM, LARGE, EXTRA_LARGE
});
}
}因此,在比较两个枚举类型的值时,不需要调用
equals()
,直接使用等号 = 即可。序列化:
序列化操作的是对象(Object),即实例化后的类(Class),
static
变量不属于任何对象,其不会被序列化。transient
关键字修饰变量,阻止该变量序列化;当对象被反序列化时,被transient
修饰的变量将会被置成类型的默认值。
接口、lambda 表达式和内部类
接口与抽象类:
共同点:
都不能被实例化;
都可以包含抽象方法;
都可以有默认实现的方法(Java8 可以用
default
关键字在接口中定义默认方法)不同点:
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。一个类用到其他的类,但是又不能访问这个类的成员,若想要交换此类的两个对象,用封装类(Wrapper Class)解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36class Car {
int model, no;
Car(int model, int no) {
this.model = model;
this.no = no;
}
void print() {
System.out.println("no = " + no + ", model = " + model);
}
}
class CarWrapper {
Car c;
CarWrapper(Car c) {this.c = c;}
}
class Main {
// 即将待操作的对象作为 wrapper 类的成员变量
public static void swap(CarWrapper cw1, CarWrapper cw2) {
Car temp = cw1.c;
cw1.c = cw2.c;
cw2.c = temp;
}
public static void main(String[] args) {
Car c1 = new Car(101, 1);
Car c2 = new Car(202, 2);
CarWrapper cw1 = new CarWrapper(c1);
CarWrapper cw2 = new CarWrapper(c2);
swap(cw1, cw2);
cw1.c.print();
cw2.c.print();
}
}解决默认方法冲突的规则
- 超类优先。若超类提供了一个具体的方法,接口中的标签相同的默认方法会被忽略。
1
2
3
4class Student extends Person implements Named {
// TODO
}
// Student 从 Person 继承了方法 getName(),则 Named 接口是否提供了 getName() 的默认实现不会有任何区别- 接口冲突。实现的两个接口提供了标签相同的方法,必须覆盖该方法以解决冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface Person {
default String getName() {
return "";
}
}
interface Named {
String getName();
}
// 编译器要求必须要解决二义性问题,即必须覆盖
class Student implements Person, Named {
public String getName() {
return "ss";
}
// TODO
}一个
lambda
表达式只在某些分支返回一个值,而在另外一些分支不返回值,是不合法的。1
(int x) -> {if (x > 0) return 1;} // ERROR
只有一个抽象方法的接口称为函数式接口(functional interface),可通过
lambda
表达式创建该接口的对象。本质上虚拟机新定义了一个
private static
方法,并return
该方法的返回值。1
2
3
4
5
6
7
8
9
10
11
12
public interface MyNumber{
public double getValue();
}
public String strHandler(String str, MyFunction mf) {
return mf.getValue(str);
}
String trimStr = strHandler("\t\t hello ", (str) -> str.trim());
String upperStr = strHandler("abcdefg", (str) -> str.toUpperCase());
String newStr = strHandler("abcdefghi", (str) -> str.substring(2, 5));lambda
表达式的体与嵌套块有相同的作用域,因此在lambda
表达式声明一个与局部变量同名的参数或局部变量是不合法的。lambda
表达式中“捕获”的变量必须是事实最终变量(effectively final),即该变量初始化后不会再为它重新赋值。1
2
3
4
5for(int i = 0; i < count; ++i) {
ActionListener listener = event -> {
System.out.println(i); // ERROR:Cannot refer to changing i
}
}局部类能够访问局部变量,但访问的局部变量必须是事实最终变量 (effectively final),且会被进行保存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void start(int internal, boolean beep) {
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println(Instant.ofEpochMilli(e.getWhen()));
if (beep) {
ToolKit.getDefaultToolkit().beep();
}
}
}
var listener = new TimePrinter();
var timer = new Timer(internal, listener);
timer.start();
}
// 编译器为这个局部内部类合成了 TalkingClock$TimePrinter
class TalkingClock$TimePrinter {
TalkingClock$TimePrinter(TalkingClock, boolean);
public void actionPerformed(java.awt.event.ActionEvent);
final boolean val$beep;
final TalkingClock this$0;
}
异常
Java 的异常层次
1
2
3
4
5
6
7Throwable
├─Error
│ └─*
└─Exception
├─IOException
├─RuntimeException
└─*将派生于
Error
类或RuntimeException
类的异常称为非检查型(unchecked)异常,其他的异常称为检查型(checked)异常。不应当声明从非检查型异常继承的异常。
超类未抛出检查型异常,子类也不能抛出任何检查型异常。
超类抛出异常,子类抛出的异常不能比超类的异常更通用,即可以抛出更特定的异常或不抛出异常。
finally
用于清理资源,而不应该有改变控制流的语句。try with resources 语句
1
2
3
4
5
6
7
8
9
10try (Resource res = ...) {
// Do with res
}
// catch 和 finally 字句非必要
catch (...) {
//
}
finally {
// ...
}Resource 继承了
AutoCloseable
接口,实现了close()
方法,则在 try 语句退出时,会自动调用res.close()
。若有
catch
、finally
字句,则在关闭资源之后执行。不要在
finally
语句中使用return
。当try
语句和finally
语句都有return
时,try
语句中的return
会被忽略。因为
try
中的return
会先暂存在一个本地变量中,执行完finally
再返回,而若finally
也有return
,这个本地变量的值就会被finally
中的return
替换。assert
实际上是抛出一个AssertionError()
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public static void main(String args[]) {
int a = 1;
int b = 1;
assert a == b;
System.out.println("yes");
assert a != b : "Noooo";
System.out.println("no");
}
--->
public static void main(String args[]) {
int a = 1;
int b = 1;
if(!$assertionsDisabled && a != b)
throw new AssertionError();
System.out.println("yes");
if(!$assertionsDisabled && a == b) {
throw new AssertionError("Noooo");
} else {
System.out.println("no");
return;
}
}
static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();
}
泛型
泛型类相当于普通类的工厂。常使用 E 表示集合的元素类型,<K, V> 表示键值对,T 或 U 表示任意类型。
定义一个带有泛型参数的方法,将类型变量放在修饰符(
public static
)的后面,返回类型的前面。1
2
3public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}虚拟机中没有泛型,只有普通的类和方法,编译器进行**类型擦除(type erasure)**,即所有的类型参数都会被替换为它们的限定类型:
1、若泛型无限定类型,如
<T>
,则替换为Object
2、若泛型有限定类型,如
<T extends A & B>
,则用第一个限定类型替换,此处为 A因此,为提高效率,应将标签(tagging)接口,即无方法的接口放到限定列表末尾,如
Serializable
。泛型的限定,多个限定类型用 & 分割,且只有第一个限定类型可以是
class
。1
2
3
4// 只有 A 可能是 class,B 和 C 必须是 interface
public static <T extends A & B & C> T fun(T.. a) {
// TODO
}编写一个泛型方法调用时,为了保持类型安全,编译器会在必要时插入强转指令。
类型擦除与多态会发生冲突,因此编译器会在子类中生成一个桥方法来调用父类中的同名方法以保持多态。
1
2
3
4
5
6
7
8
9
10DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
/*
* 由于类型擦除,虚拟机中的 setSecond(LocalDate) 无法覆盖 setSecond(Object),
* 因此编译器在 DateInterval 类中生成了如下桥方法
*/
public void setSecond(Object second) {
setSecond((LocalDate) second);
}在虚拟机中,由参数类型和返回类型共同指定一个方法。
无论 S 与 T 有什么关系,
Pair<S>
与Pair<T>
都没有任何关系。泛型通配符的使用规则:PECS,即生产者使用 <
? extends T>
,消费者使用 <? super T>
。Class
类是泛型的,例如String.class
实际上是唯一的Class<String>
类的对象。“ for each ” 循环的原理就是使用了普通的 for 循环和迭代器,可以处理任何实现了
Iterable
接口的对象,该接口只包含一个抽象方法:1
2
3public interface Iterable<E> {
Iterator<E> interator();
}
集合
Java 的迭代器位于两个元素之间,当调用
next
时,迭代器越过下一个元素,并返回刚刚越过的元素的引用,remove()
删除上次调用next()
所返回的元素。集合有两个基本接口:
Collection
和Map
。Java 中的所有链表都是双向链接。
动态数组的选用:不需要同步时,用
ArrayList
;需要同步时,用vector
。HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素。对于 Map,要迭代处理 key 和 value,用
forEach()
,提供一个接受键和值的 lambda 表达式1
map.forEach((k, v) -> System.out.println(k, v))
Queue
是单端队列,只能从一端插入元素,另一端删除元素,根据因为容量问题而导致操作失败后处理方式的不同可以分为两类方法:一种在操作失败后会抛出异常,另一种则会返回特殊值。功能 抛出异常 返回特殊值 插入队尾 add(E e) offer(E e) 删除队首 remove() poll() 查询队首元素 element() peek() Deque
提供有push()
和pop()
等其他方法,可用于模拟栈。ArrayDeque
是基于可变长的数组和双指针来实现,不支持存储NULL
数据,当被用作栈时比Stack
快,当被用作队列时比queue
快。ArrayList
(JDK8)HashMap
通过 key 的hashCode()
经过扰动函数处理扩散到高位后得到 hash 值,然后通过(n - 1) & hash
判断当前元素存放的位置),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。之所以用 (n - 1) 是因为 n 规定为 2 的整数次幂,n 的二进制表示最低位一定为 0,与 hash 进行按位与操作后最低位仍为 0,这就导致 i 值只能为偶数,浪费了数组中索引为奇数的空间,同时增加了 hash 冲突发生的概率。
计算哈希时之所以要右移 16 位,是因为当数组长度 n 较小时,n-1 的二进制数高 16 位全部为 0,只能利用到 h 的低 16 位数据,提高了 hash 冲突发生的可能性。
1
2
3
4static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}拉链数组长度大于 8 时,调用
treeifyBin()
方法,然后根据数组长度是否大于等于 64 ,执行红黑树转换或扩容操作。并发
用
ReentrantLock
保护代码块,必须在 finally 字句中释放锁,否则若临界区代码抛出异常,别的进程将会被阻塞1
2
3
4
5
6mylock.lock();
try {
// TODO
} finally {
mylock.unlock();
}一个锁对象可以有一个或多个相关联的条件对象,用
newCondition()
获取一个条件对象。当条件不满足时,调用条件对象的await()
方法,进入这个条件的等待集,直到锁可用,另一线程在同一条件上调用signalAll()
方法,接触等待线程的阻塞,使这些线程在当前线程释放锁后竞争访问对象。若一个方法声明时有
synchronized
关键字,对象的锁将保护整个方法1
2
3
4
5
6
7
8
9
10
11
12public synchronized void fun() {
// TODO
}
等价于
public void fun() {
this.intrinsicLock.lock();
try {
// TODO
} finally {
this.intrinsicLock.unlock();
}
}最好用
java.util.concurrent
来处理锁定,而不要用Lock
和synchronized
,除非需要代码简洁或需要锁提供额外的能力。如果声明一个字段为
volatile
,那么编译器和虚拟机就知道该字段可能会被另一个线程并发更新。编译器会插入适当的代码,使得某一个线程对该变量做修改时,这个修改对读取这个变量的其他线程都可见。ThreadLocal
类为各个线程提供一个单独的生成器,让每个线程访问对都是存储在线程内部自己的变量副本。
代理
静态代理
静态代理中,对目标对象的每个方法的增强都是手动完成的,非常不灵活(接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少。对于 JVM,静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入代理类;
- 在代理类的对应方法调用目标类中的对应方法。
eg:
1 | // 1、定义一个接口及其实现类; |
动态代理
动态代理是在运行时动态生成类字节码。动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理等等。
JDK 动态代理机制
主要用到 Proxy
类的 newProxyInstance()
方法,通过该方法创建的代理对象在调用方法时,实际执行 h
的invoke()
方法。只能代理实现了接口的类。
1 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) |
实现步骤:
- 定义一个接口及其实现类;
- 自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中调用原生方法(被代理类的方法)并自定义一些处理逻辑; - 通过
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象。
eg:
1 | // 1、定义一个接口及其实现类; |
CGLIB 动态代理机制
CGLIB(Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许在程序运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。
实现步骤:
- 定义一个待代理的类;
- 自定义
MethodInterceptor
并重写intercept()
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似; - 通过
Enhancer
类的create()
创建代理类。
使用前需要添加相关依赖
1 | <dependency> |
eg:
1 | // 1、定义待代理的类 |
IO
IO 模型:
BIO(Blocking IO):同步阻塞 IO。应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间
NIO(Non-blocking/New IO):IO 多路复用模型。传统的同步非阻塞 IO 模型如下:应用程序会一直发起 read 调用轮询数据是否已经准备好,内核准备数据就绪后,线程阻塞,直到数据从内核空间拷贝到用户空间。IO 多路复用模型对同步非阻塞 IO 模型进行优化:线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用
AIO(Asynchronous I/O):NIO 2,异步 IO 模型。基于事件和回调机制实现,程序发起 read 后立即返回,内核处理完成后,操作系统会通知相应的线程进行后续的操作
所有的 IO 流都派生于如下四个抽象基类:
InputStream
/InputReader
: 所有字节输入流的基类/所有字符输入流的基类OutputStream
/OutputWriter
: 所有字节输出流的基类/所有字符输出流的基类一般不会直接单独使用
FileInputStream
,通常会配合BufferedInputStream
来使用(字节缓冲流会先将读取到的字节存放在缓存区,大幅减少系统调用次数,提高读取效率,不是减少磁盘 IO 操作次数(这个 OS 已经帮我们做了)),或者配合其他的 Stream,即设计模式中的装饰器模式。1
2
3
4
5
6
7
8
9
10
11// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
// 读取文件的内容并复制到 String 对象中
String result = new String(bufferedInputStream.readAllBytes());
System.out.println(result);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
//可以读取任意具体的类型数据
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();FileOutputStream
类似,通常配合其他类使用1
2
3
4
5
6
7
8
9
10
11FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
// 输出任意数据类型
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("output.txt")
Person person = new Person("zhangsan", 18);
output.writeObject(person);InputStreamReader
是字节流转换为字符流的桥梁,其子类FileReader
是基于该基础上的封装,可以直接操作字符文件,即设计模式中的适配器模式1
2
3
4// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {}
// 用于读取字符文件
public class FileReader extends InputStreamReader {}OutputStreamWriter
是字符流转换为字节流的桥梁,其子类FileWriter
是基于该基础上的封装,可以直接将字符写入到文件1
2
3
4// 字符流转换为字节流的桥梁
public class OutputStreamWriter extends Writer {}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {}RandomAccessFile
中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置,常见的一个应用就是实现大文件的断点续传