忙里偷闲,扎实基础才是正经事。 8说了,开冲!
1. JDK和JRE有什么区别
这个知识点我想只要搞Java的都知道……
- JDK:Java Development Kit的简称,Java开发工具包,提供了Java的开发环境和运行环境(JRE)
- JRE: Java Runtime Environment 的简称,Java 运行环境
JDK之中其实包含了JRE还有javac,一个用来编译源码的编译器(.java->.class),还有很多相关的工具。
2. == 和 equals 的区别是什么
首先,在java之中我们都知道数据分为两种类型:基本类型,包括 integer,boolean 等等,还有引用类型(各种Object)。 对于两种类型而言,== 的作用是不同的,差别如下:
- 基本类型:比较值是否相同
- 引用类型:比较引用是否相同 下面是代码实例:
String x= "String";
String y= "String";
String z= new String("String");
System.out.println(x==y);//This one is true because they are both basic type
System.out.println(x==z);//This one is false because it is comparision between the basic type and object.
System.out.println(x.equls(y));//This one returns true,because the function "euqals" only compare the value between these two.
System.out.println(x.equls(z));//This one returns true,because the function "euqals" only compare the value between these two.
解读如下: 因为x和y指向 是同一个引用,所以 == 是 true。但是 new String() 方法则重新开辟了内存空间,所以 == 结果是 false, 但是 equals 比较的一直是值,所以其最后都为 true **equals 解读 ** equals 本质就是 == , 但是 String 和 Integer 等等重写了 equals 方法,将其变成了值比较。 首先看默认情况下equals 比较一个有相同值的对象:
class Cat{
public Cat(String name){
this.name=name;
}
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
}
Cat c1=new Cat("Meow");
Cat c2=new Cat("Meow");
System.out.println(c1.equals(c2));//false
结果最后是fasle,原因是:
public boolean equals(Object obj){
return (this == obj);
}
所以 equals 本质上就是 == 那为什么两个相同值的 String 对象,返回的却是 true ? 因为之前我们提到过的, Java 将整个 String 之中的 equals 方法重写,变成了下面的代码:
public boolean equals(Object anObject){
if(this == anObject){
return true;
}
if(anObject instanceof String){
String anotherString = (String)anObject;
int n = value.length;
if(n == anotherString.vakue.length){
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0){
if(v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
3. 两个对象的 hashCode() 相同,则 equals() 也一定为 true, 对吗?
不对。两个对象的 hashCode() 相同,equals() 不一定为 true。 代码示例:
String str1 = "通话";
String str2 = “重地;
System.out.pringln(String.format("str1:%d | str2: %d",str1.hashCode(),str2.hashCode()));
System.out.println(str1.equals(str2));
执行的结果:
str1: 1179395 | str2: 1179395
false
可以看到,”通话“和”重地“的hashCode()是相同的,然而equals()则是 false,因为在 HashTable之中, hashCode() 相等只是说明二者的和Hash 相同,但是 Hash值相同,并不能得出键值对相等。
4. final 在 Java 之中有什么作用?
- final修饰的类叫最终类,该类不可以被继承
- final修饰的方法不可以被overwrite,但是可以被继承(方法可以,类不可以)
- final修饰的变量叫做常量,常量必须被初始化,初始化之后的值就不可被修改
- final不能用于修饰构造方法
注:父类的private成员方法是不能被子类方法覆盖的,因此父类之中private类型的方法默认是final类型的
上面的这三种是比较抽象的说法,下面我就 final 的意义做具体的讲述。
参照博客: https://blog.csdn.net/andie_guo/article/details/12885885
Java 关键字 final 有”这是无法改变的“,或者”终态的“含义。其可以修饰非抽象类,非抽象类成员方法和变量。可以出于两种理解阻止改变:设计或者效率。
1. final数据
- 一个永不改变的编译时常量
- 一个在运行时被初始化的值,而之后无法改变
- 一个既是static又是final的域,是一段不能改变的存储空间
所以在数据部分,final 的作用是提供一个永远不会变化的常量,但是其在不同的数据类型之中表现也不相同:
- 基本数据类型: final 让 value 保持不变
- 对象引用(object reference): final仅仅让 reference 保持不变,也就是该指针不可以指向其他对象,但是所指向的对象本身内容可以改变。
- 数组类型使用final时,final的使用使数组引用很顶不变,数组内部的数据如果不是final型,可以进行修改。(和上述的对象引用类似)
final和static的差别
- final指明数据为一个常量,恒定无法修改
- static指明数据只占用一份内存区域
public class FinalData {
private final int valueOne = 3;
private int valueTwo = 4;
private final Value v1 = new Value(4);
private Value v2 = new Value(10);
private final int[] a = {1,2,3,4,5,6,7,8,9};
private int[] b = {1,2,3,4,5,6,7,8,9};
private static final int VAL_TWO = 3;
public static void main(String[] args) {
FinalData finalData = new FinalData();
/*-----------基本类型测试------------------------------------*/
// finalData.valueOne = 4;//valueOne是常量,无法修改
finalData.valueTwo = 14;//valueTwo不是常量,可以修改
/*-----------对象类型测试------------------------------------*/
// finalData.v1 = new Value(5);//v1对象是final型常量,其引用是无法修改的。
finalData.v2 = new Value(20);//v2对象final型常量,其引用可以修改。
finalData.v1.i = 5;//v1对象的成员变量不是final型,可以修改
/*-----------数组类型测试------------------------------------*/
// finalData.a = new int[3];//数组a是final型,无法修改a的引用
finalData.b = new int[13];//数组b不是final型,可以对其引用进行修改
for(int i=0;i<finalData.a.length;i++)
finalData.a[i]++;//数组a内部数据是int型,不是final型,可以修改
/*-----------static final类型测试------------------------------------*/
// finalData.VAL_TWO = 4;
//定义为private,只能被本类的方法调用;定义为static,则强调只有一份,且只被执行一次;定义为final,则说明它是一个常量,无法被修改。
}
}
2. final方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。其原因为:
- 从程序员的角度而言,把方法锁定,防止任何继承修改它的意义和实现
- 从整个程序的角度而言,可以使得程序的效率更高。编译器在遇到调用 final 方法的时候会转入内嵌机制,大大提高执行效率。
下面是代码示例:
public class FinalDemo {
public void f(){
System.out.println("FianlDemo.f()");
}
public final void g(){
System.out.println("FianlDemo.g()");
}
}
public class FinalOverriding extends FinalDemo{
public void f(){
System.out.println("FinalOverriding.f()");
}
// public void g(){//无法覆盖父类的final方法g()
// System.out.println("FinalOverriding.g()");
// }
}
3.final 类
final 类不可以被继承,因此 final 类的method 不会被覆盖,默认都是final的。在设计类时候,如果:
- 这个类不需要有子类
- 类的实现细节不允许改变
- 类不会再被扩展
那么就设计为 final 类。
5. Java 之中的 Math.round(-1.5) 等于多少?
结果是 -1。
在数轴上面取值的时候,中间值(0.5)向右取整,所以 +0.5 是向上取整, -0.5 是直接舍弃。
6. String属于基础的数据类型么?
String 不是基本的数据类型,基础类型只有八种:
Byte, Boolean,Char,Short,Int,Float,Long,Double。
String属于对象
7. Java 之中操作字符串都有哪些类?它们之间有什么区别?
操作字符串的类有: String,StringBuffer,StringBuilder.
String和StringBuffer,StringBuilder的区别在于:String声明的是不可变的对象,每次操作都会生成新的对象,然后将指针指向新的对象。
但是StringBuffer,StringBuilder就是每次都在原有对象的基础之上进行操作,所以如果经常改变String内容的情况下,最好不要使用String,这样会导致不断的创建新的对象,那么效率就会大大降低。
StringBuffer和StringBuilder的区别在于,StringBuffer是线程安全的,但是这种线程安全的保证就是最基础的加锁,所以在不需要线程安全的情况下,最好使用StringBuilder,这样性能更高。
8.String str = "i"
和 String str = new String("i")
一样吗?
不一样。
在之前的有一篇blog之中讲过JVM之中不同的内存分配方式。
在方法区之中的运行时常量池(Runtime Constant Pool) 是方法区的一部分,用于存储编译期就已经生成的字面常量,符号引用,翻译出来的直接引用等等。所以String str = "i"
会被分配到常量池之中。而堆内存,作为JVM所管理内存之中最大的一个部分,第二种方式创建的String str = new String("i");
会被直接分配到堆内存之中。
9. 如何将字符串反转?
使用 StringBuilder 或者 StringBuffer 的 reverse() 方法。
10. String类常用的方法都有哪些?
- indexOf(): 返回指定字符的索引
- charAt(): 返回指定索引处的字符
- replace(): 字符串替换
- trim(): 去除字符两端空白
- split():分割字符串,返回一个分割后的字符串数组
- getBytes(): 返回字符串的 byte 类型数组
- length(): 返回字符串长度
- toLowerCase():将字符串转换成小写字母
- toUpperCase(): 将字符串转换成大写字母
- substring(): 截取字符串
- equals(): 字符串比较
11. 抽象类必须要有抽象方法吗?
https://www.jianshu.com/p/0530e14192b4
首先说一下抽象类和抽象方法:
11.1 抽象类
- 为什么有抽象类?
我们都知道,父类是将子类之中所共同具有的属性和方法进行抽取,但是很多方法在确定父类的情况下并不能提前确定其实现,那么这种情况下,我们暂时将其定义为抽象,在以后的子类再进行继续的重用。
所以抽象类的意义就在于此: 将相同的但是不确定的特点先提取出来,为了以后的重用。定义成抽象类的目的,就是在子类当中实现抽象方法。
- 抽象类和抽象方法的区别?
有抽象方法的类就是抽象类,但是抽象类之中也可以不包含抽象方法。抽象类的声明使用abstract关键字。
- 抽象类的基本特性和使用方法
- 抽象类不可以被实例化,因为抽象类之中可能具有抽象方法(之后会提及抽象类和抽象方法之间的关系),所以抽象类算是一种不完整的类,直接实例化就失去意义了。
- 要使用抽象类,就必须有子类,使用extends继承。一个子类只可以继承一个抽象类。
- 如果抽象类的子类不是抽象类,那么就必须复写抽象类之中的所有抽象方法。如果子类没有实现父类的抽象方法,那么必须将子类也定义为 abstract 类。
- 抽象类的使用限制
- 抽象类可以有构造方法。由于抽象类也是一个类,内部可以存在一些属性,那么抽象类之中也可以有构造方法,其目的是为了属性的初始化。且子类对象实例化的时候,也满足先执行父类构造,再执行子类构造的顺序。也就是说,在子类和父类这个情况之下,抽象类并没有什么特殊。
- 抽象类不可以使用 final 声明。这个原因我们之前有讲到过,那就是 final 定义的类不可以有子类,但是抽象类的实现方法就是要依靠子类,所以不可以。
- 抽象类能否使用 static 声明?
外部抽象类不允许使用 static 声明,但是内部抽象类可以使用 static 声明。使用 static 声明的内部抽象类相当于一个外部抽象类,继承的时候使用”外部类.内部类”的形式表示类名称。
内部抽象类使用示例:
abstract class A{
//static定义的内部类属于外部类
static abstract class B{
public abstract void print();
}
}
class C extends A.B{
public void print(){
System.out.println("**********");
}
}
public class TestDemo {
public static void main(String[] args) {
//向上转型
A.B ab = new C();
ab.print();
}
}
结果:
**********
可见这种情况之中,直接使用 A.B 来使用当前的类。
- 抽象类之中的 static 方法可以直接调用
下面是示例代码:
abstract class A{
public static void print(){
System.out.println("Hello World !");
}
}
public class TestDemo {
public static void main(String[] args) {
A.print();
}
}
结果如下:
Hello World !
上面的代码之中即为直接使用抽象类之中的 static 方法,从而直接输出的例子。并没有任何初始化整个 Class 的行为。
- 抽象类之中,如果只需要一个特定的系统子类操作,那么可以忽略掉外部子类。这样的设计作用为对用户隐藏不需要知道的子类。
示例如下:
abstract class A{
public abstract void print();
//内部抽象类子类
private static class B extends A{
//覆写抽象类的方法
public void print(){
System.out.println("Hello World !");
}
}
//这个方法不受实例化对象的控制
public static A getInstance(){
return new B();
}
}
public class TestDemo {
public static void main(String[] args) {
//此时取得抽象类对象的时候完全不需要知道B类这个子类的存在
A a = A.getInstance();
a.print();
}
}
结果为:
Hello World !
在上面的代码之中, main 函数之中直接使用 getInstance() 的方法得到了其内部的一个抽象类子类,但是完全不知道 B 子类的存在。
11.2 抽象方法
- 抽象方法和普通方法的区别
在普通方法上面都会有一个’{}’,来表示方法体。有方法体的方法一定可以直接被对象引用。
抽象方法,指的是
- 没有方法体的方法
- 还必须使用关键字 abstract 作为修饰。
- 抽象方法必须为 public 或者 protected。因为如果是 private,则不可以被子类继承,子类就无法实现该方法。默认情况是 public
抽象方法一例:
//没有方法体,有abstract关键字做修饰
public abstract void xxx();
所以本题的答案就出来了:抽象类不一定要有抽象方法。下面是示例:
abstract class Cat{
public static void sayHi(){
System.out.println("hi~");
}
}
上面的这个例子里面,抽象类并没有任何抽象方法,但是依旧可以正常运行。
12. 普通类和抽象类有何区别?
上面的抽象类介绍之中都已经将这些讲清楚,下面简要说一下答案。
- 普通类不能包含抽象方法,抽象类可以包含抽象方法
- 抽象类不能直接实例化,但是普通类可以直接实例化。
13. 抽象类可以用 final 修饰吗?
答案上面提及过。不可以。
14. 接口(interface) 和抽象类(abstract class) 有什么区别?
参考链接: https://blog.csdn.net/chenssy/article/details/12858267
14.1 什么是接口(interface)
上面讲过了抽象类,那么这里将接口是什么也好好梳理一下:
首先要澄清的一点是,接口不是类,从我们不可以实例化一个接口就可以看出来这一点。
接口,是用来建立类与类之间的协议,其所提供的仅仅是一种形式,不是其具体的实现。同时该接口的实现类,必须要实现该接口的所有方法。通过 implement 关键字,其表示该类在遵循某些指定的接口,并且也表示着:”interface 只是其外观,但是现在要声明其是如何工作的”。
接口,是抽象类的延申,java 为了保证数据安全,是不可以多重继承的,也就是说继承只可以存在一个父类。但是接口不同,一个类可以同时 implement 多个接口,不管这些接口之间有没有关系。所以接口弥补了抽象类不可以多重继承的缺陷。
在使用接口的过程之中需要注意以下几个问题:
- Interface的所有方法访问权限被自动声明为 public, 确切的说只能为 public。
- 接口之中可以定义“成员变量”,或者可以说是不可变的常量。因为接口之中的“成员变量”会自动变为 public static final。在接口之中的成员变量可以通过类命名直接访问:ImplementClass.name
- 接口之中不存在实现的方法。
- 实现接口的非抽象类必须要实现该接口的所有方法,但是抽象类可以不用实现。
- 在实现多接口实现的时候一定要避免方法的重复。
14.2 抽象类和接口在具体方向之上的区别
下面从语法层次和设计层次两个方面来对抽象类和接口进行阐述。
14.2.1 语法层次
在语法层次,java 对抽象类和接口分别给出了不同的定义。下面用 Demo 类来说明其之间的不同之处。
使用抽象类来实现:
public abstract class Demo {
abstract void method1();
void method2(){
//实现
}
}
使用接口来实现:
interface Demo {
void method1();
void method2();
}
可见,在抽象类之中,抽象类可以有任意范围的成员数据,同时也可以有自己的非抽象方法。但是在 interface 之中,只可以有静态,不可以修改的成员数据,尽管在接口之中我们一般不使用成员数据。同时,在 interface 之中的方法都必须是抽象的,不可以有哪种方法的具体实现。
14.2.2 设计层次
从设计理念来彻底剖析二者区别,可以得到:
-
抽象层次不同。抽象类,是对类抽象;而接口是对行为的抽象。抽象类,要对整个类的整体做抽象,包括属性和行为等等,但是接口,却是对类的行为部分做抽象。
-
跨域不同。我们之前提到过,抽象类所抽象的是具有相似特点的类,但接口却可以横跨不同的类。抽象类是从子类之中发现公共部分,然后泛化,子类可以直接继承抽象类。但是接口则不同,实现接口的子类可以不存在任何关系,例如鸟,飞机等等都可以实现飞Fly接口,但是不可以将其归为一个父类。
从这个角度来看,抽象类体现的是一种继承关系,父类和派生类之间必须有“is-a”关系,但是对于接口则不然,并不要求接口的实现和接口的定义在概念上是一致的,仅仅是实现接口所规定的功能即可。
设计层次不同。对于抽象类而言,其是自下而上设计的,要先知道子类,才能从子类之中抽象出父类;但是对于接口则不同,接口不需要知道子类的存在,只要定义一个规则即可。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
14.3 简答
分析完上面的各个特性,下面是简答:
- 实现(上面说的语法层面):抽象类的子类使用extends来继承,接口则必须使用implements 来实现接口
- 构造函数: 抽象类可以有构造函数;接口则不可以(一个是概括某些类的特性,一个则是约定的规则而已)
- 实现数量:类可以实现多个接口,但是只可以继承一个抽象类
- 访问修饰符: 接口之中的方法默认使用 public 修饰,抽象类的方法可以是任何访问修饰符
15. Java 中 I/O 流分为几种?
- 按照功能来分:输入流(input),输出流(Output)
- 按照类型来分: 字节流和字符流
其区别在于:字节流是按照8位传输,以字节为单位输入输出数据,字符流按照16位传输,以字符为单位输入输出数据。
16. BIO,NIO,AIO有什么区别
下面是参考链接:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/BIO-NIO-AIO.md
也强力推荐上面的这个教程,里面涵盖了大部分Java 相关的知识难点总结和辨析。
首先回顾下面的几个概念:同步 和 异步,阻塞 和 非阻塞。
同步和异步的区别:
- 同步:发起一个调用之后,被调用者 未处理完请求之前,调用不返回
- 异步:发起一个调用之后,立刻得到被调用者的回应,表示已经接收到请求,但是被调用者并没有返回结果。这个时候,可以处理其他的请求。被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别,最大就在于异步的话不需要等待处理结果,可以在等待期间做其他的事务。被调用者会利用其他机制,比如回调等等来 通知 调用者其返回结果。
那么 阻塞 和 非阻塞 的区别就很明显了。
- 阻塞:发起请求之后一直等待请求返回。在等待期间不进行任何任务
- 非阻塞:发起请求之后不需要等请求的全部内容返回,可以去进行其他任务。
在作者举得例子里面,我认为 同步非阻塞 和 异步非阻塞 的主要区别在于状态的获取方法。同步阻塞 之中,需要进程不断的去主动获取状态。 而 异步非阻塞 之中,状态会被请求主动返回,因此在这个期间进程不需要去不断获取主动状态,而是等待请求主动通知其状态已经改变再进行动作。
16.1 什么是Java事件机制
参考部分:https://zhuanlan.zhihu.com/p/27185877
http://www.laphilee.com/posts/42720.html
Java 事件机制之中涉及到和事件处理相关的类有下面几个部分:
- EventObject
- EventListener
- Source
下面是分别的介绍:
-
EventObject
其继承于
java.util.EventObject
,是事件状态对象的母类。其封装了事件的源对象和事件相关的信息,所有的 java 事件类都需要继承该类。也就是说,其代表的是“事件对象”,在下文之中会提及如何去使用。
-
EventListener
EventListener 只是一个接口,其内部没有任何方法。所有的事件监听器都需要实现该缺口。事件监听器注册在事件源之上,当事件源的属性,或者状态改变的时候,调用相应监听器之中的回调方法。
相当于
EventListener
是由Source
的属性变化所触发。 -
Source
Source
,也是事件源。事件源,是事件最初发生的地方。因为事件源要注册EventListener
,所以事件源之内要有调用EventListener
的容器。
上面三者的关系是,EventObject1
和EventListener
都在Source
里面,由Source
来调用。Source
会在EventObject
发生变化的时候,通知在EventListener
之中的所有事件发生。
16.2 BIO(Blocking I/O)
BIO,也就是 同步阻塞I/O模式, 数据的 read 和 write 必须在一个线程之内等待其完成。
16.2.1 传统 BIO
下面是传统BIO的模型图,为 一请求一应答。
可见其对于一个客户端就有一个线程。
采用 BIO通信模型 的 Server, 如上图所示,有一个独立的 Acceptor 线程来监听客户端的连接。 一般, 在 while(true)
循环之中,服务端会调用 accept()
方法来等待接收客户端的连接。 请求一旦接收到一个连接,就可以建立 通信socket ,并且在这个 socket 上面进行操作。这个时候, 无法接受其他的客户端的连接请求, 只能等待当前链接的客户端的操作完成。 如果想要对多个客户端支持连接,可以通过多线程的方式。
我们上面提到了多线程,必须使用多线程的原因是 socket.accept()
, socket.read()
,socket.write()
这三个函数都是同步阻塞的。那么多线程有两种方式:
- 最基本的方式就是,每次接收到 client 的连接请求之后,就要为这个 client 创建 一个线程进行处理,处理完成 之后,再通过 Output Stream 来返回给客户端,再将这个新创建的线程销毁。
- 既然多线程,那么第一个想到的就是 线程池机制, 这样创建和回收线程的成本都比较低。使用 FixedThreadPool ,实现线程数量的控制。
要是 client 并发访问量增加之后,第一种方式会出现什么问题?
之前总结过的,在 JVM 之中,线程是非常宝贵的资源。线程的创建成本和销毁成本都是重量级的。那么并发访问量急剧增加,可能会导致:
- 堆栈溢出
- 创建新线程失败
等等问题,最后导致宕机。
16.2.2 伪异步 IO
为了解决之前所说的,同步阻塞I/O 面临的链路需要线程处理的问题,有人采用了我们上面讲到过的第二种方法进行了优化:后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M, 线程池最大线程数N的比例关系。其中的M可以远远大于N(我们上面提到过,可以使用 FixedThreadPool
来实现线程数量的控制,这样可以避免海量并发接入导致线程耗尽)
下面是模型图:
原理是:
采用线程池和任务队列,可以实现一种叫做 伪异步 的 I/O 通信框架。 它的模型图如上图所示。每当有新的 client 接入, 将客户的 socket 封装成一个 Task, 投递到后端的线程池之中进行处理。JDK 的线程池维护一个 MQ 和 N 个活跃线程。这样,由于线程池的资源占用是可控的,无论多少个 client 进行访问,都不会导致资源的耗尽。
虽然 伪异步I/O 通信框架采取了线程池实现,因此避免了为每个请求都创建一个独立线程所导致的资源耗尽问题,但是底层原理仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。
16.2.3 总结
在活动连接数不是很高的情况之下,这种模型也是比较不错的,相对而言更简单,且每个连接可以专注于自己的I/O。但是面对十万甚至百万级连接的情况下,BIO 模型是无能为力的,因此需要一种更高效的 I/O 处理模型来应对更高的并发量。
16.3 NIO(New I/O)
16.3.1 NIO 简介
NIO 是一种同步非阻塞的I/O模型,在 Java 1.4 之中引入了 NIO 框架,对应 java.nio 包。 提供了 Channel, Selector, Buffer 等等抽象。
NIO 支持面向 buffer 的,基于 channel 的I/O 操作方法。NIO 提供了与传统 BIO 模型之中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的 Socket 实现, 两种通道都支持 阻塞 和 非阻塞 两种模式。 阻塞模式和传统之中的支持一样,比较简单,但是性能和可靠性都不好。 非阻塞模型就与其相反。
16.3.2 NIO 的特性,NIO和IO的区别
总结: NIO 流是非阻塞 IO, 但是 IO 流是阻塞 IO。
-
Non-Blocking IO(非阻塞IO)
IO 流是阻塞的, NIO 流是不阻塞的。
Java NIO 使我们可以进行非阻塞IO操作,比如,单线程之中从通道读取数据到 buffer, 同时进行其他任务的处理。当数据被读取到 buffer 之中后,线程再继续处理数据。 写数据也是一样的。另外,非阻塞写 也是如此,一个线程请求写入一些数据到某通道,但是不需要等待其完全写入,线程同时可以去做其他的事情。
Java IO 的各种流是阻塞的,这意味着,如果一个线程使用
read()
或者Write()
的时候,线程就会被阻塞,直到数据被读取或者写入。在这个期间,该线程就不能做任何事情了。 -
Buffer(缓冲区)
IO面向流(Stream Oriented),而 NIO 面向缓冲区(Buffer oriented)
Buffer 是一个对象,其包含一些要写入或者要读出的数据。
在面向流的 I/O (Stream Oriented) 之中,可以将数据直接写入或者读到 Stream 对象之中。虽然 Stream 之中也可以有 Buffer 开头的扩展类,但是其只是流的包装类,还是从流到缓冲区。但是 NIO 却是直接读到 Buffer 之中进行操作。
在 NIO 库之中,所有数据都是用缓冲区处理的。
最常用的缓冲区是 ByteBuffer, 一个 ByteBuffer 提供一组功能用于操作 Byte 数组。除了 ByteBuffer, 还有其他的一些缓冲区,事实上,每一种 Java 基本类型,除了 Boolean, 都有一种对应的缓冲区。
-
Channel (通道)
NIO 通过 Channel 来进行读写。
通道是双向的,可读,也可写。而流的处理是单向的。无论读写,通道只能和 Buffer 交互,因为 Buffer, 通道可以异步的读写。
-
Selector (选择器)
NIO 有选择器,但是 IO 没有。
选择器用于使用单个线程来处理多个通道。Selector 用来提高系统的效率。
16.3.3 NIO读数据和写数据方式
通常而言,NIO 之中的所有 IO 都是从 Channel 开始的。
- 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据
- 从通道进行数据写入:创建一个缓冲区, 填充数据,并且要求通道写入数据。
下面是具体的操作图示:
16.3.4 NIO 核心组件简单介绍
NIO 之中包含下面几个核心的组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
这三个是提及到的NIO 体系的核心概念。
但是大家都不太喜欢使用 Java 原生 NIO 来做开发,除了编程复杂,编程模型难之外,其还有下面这些让人诟病的问题:
- JDK 的 NIO 底层使用 epoll 实现,空轮询bug会导致CPU飙升至100%.
https://www.jianshu.com/p/3ec120ca46b2
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug, 维护成本较高。
16.4 AIO(Asynchronous I/O)
AIO,是在 Java 7 之中引入的 NIO 的改进版本,其为 异步非阻塞 的 IO模型。异步 IO 是基于事件和回调机制实现的,不会发生堵塞,而是当后台处理完成,操作系统 会通知相应的线程进行后续的操作。
AIO 是异步 IO 的缩写。虽然在 NIO 的网络操作之中,提供了非阻塞的方法,但是 NIO 的IO 行为还是同步的。 对于 NIO 而言,业务线程是在 IO 操作准备好的时候,得到通知,接着由通知到的线程本身进行 IO操作,IO操作本身是同步的。
16.5 简答
- BIO:Block IO , 同步阻塞式 IO, 就是平常传统使用的 IO。其特点是模式简单,使用方便,但是并发处理能力较低(我们上文提及过,线程是同步阻塞的,所以效率很低)
- NIO:New IO,同步非阻塞 IO, 是传统 IO 的升级,Client 和 Server 通过 Channel 进行通讯,实现了多路复用
- AIO:Asynchronous IO,是 NIO 的升级,也叫 NIO2, 实现了异步非阻塞 IO,异步 IO的操作基于事件和回调机制。
17. Files 的常用方法有哪些?
- Files.exists():检测文件路径是否存在
- Files.createFile():创建文件
- Files.createDirectory(): 创建文件夹
- Files.delete(): 删除一个文件或者目录
- Files.copy(): 复制文件
- Files.move(): 移动文件
- Files.size(): 查看文件个数
- Files.read(): 读取文件
- Files.write(): 写入文件