Java相关知识点梳理(一)

Java基础,包括Java环境,Java基本类型,抽象类与接口等等

Posted by Haiming on September 4, 2019

忙里偷闲,扎实基础才是正经事。 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 抽象类

  1. 为什么有抽象类?

我们都知道,父类是将子类之中所共同具有的属性和方法进行抽取,但是很多方法在确定父类的情况下并不能提前确定其实现,那么这种情况下,我们暂时将其定义为抽象,在以后的子类再进行继续的重用。

所以抽象类的意义就在于此: 将相同的但是不确定的特点先提取出来,为了以后的重用。定义成抽象类的目的,就是在子类当中实现抽象方法。

  1. 抽象类和抽象方法的区别?

有抽象方法的类就是抽象类,但是抽象类之中也可以不包含抽象方法。抽象类的声明使用abstract关键字。

  1. 抽象类的基本特性和使用方法
  • 抽象类不可以被实例化,因为抽象类之中可能具有抽象方法(之后会提及抽象类和抽象方法之间的关系),所以抽象类算是一种不完整的类,直接实例化就失去意义了。
  • 要使用抽象类,就必须有子类,使用extends继承。一个子类只可以继承一个抽象类。
  • 如果抽象类的子类不是抽象类,那么就必须复写抽象类之中的所有抽象方法。如果子类没有实现父类的抽象方法,那么必须将子类也定义为 abstract 类。
  1. 抽象类的使用限制
  • 抽象类可以有构造方法。由于抽象类也是一个类,内部可以存在一些属性,那么抽象类之中也可以有构造方法,其目的是为了属性的初始化。且子类对象实例化的时候,也满足先执行父类构造,再执行子类构造的顺序。也就是说,在子类和父类这个情况之下,抽象类并没有什么特殊。
  • 抽象类不可以使用 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 抽象方法

  1. 抽象方法和普通方法的区别

在普通方法上面都会有一个’{}’,来表示方法体。有方法体的方法一定可以直接被对象引用。

抽象方法,指的是

  • 没有方法体的方法
  • 还必须使用关键字 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 多个接口,不管这些接口之间有没有关系。所以接口弥补了抽象类不可以多重继承的缺陷。

在使用接口的过程之中需要注意以下几个问题:

  1. Interface的所有方法访问权限被自动声明为 public, 确切的说只能为 public。
  2. 接口之中可以定义“成员变量”,或者可以说是不可变的常量。因为接口之中的“成员变量”会自动变为 public static final。在接口之中的成员变量可以通过类命名直接访问:ImplementClass.name
  3. 接口之中不存在实现的方法。
  4. 实现接口的非抽象类必须要实现该接口的所有方法,但是抽象类可以不用实现
  5. 在实现多接口实现的时候一定要避免方法的重复。

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 设计层次

从设计理念来彻底剖析二者区别,可以得到:

  1. 抽象层次不同。抽象类,是对类抽象;而接口是对行为的抽象。抽象类,要对整个类的整体做抽象,包括属性和行为等等,但是接口,却是对类的行为部分做抽象。

  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的容器。

上面三者的关系是,EventObject1EventListener都在Source里面,由Source 来调用。Source 会在EventObject发生变化的时候,通知在EventListener之中的所有事件发生。

16.2 BIO(Blocking I/O)

BIO,也就是 同步阻塞I/O模式, 数据的 read 和 write 必须在一个线程之内等待其完成。

16.2.1 传统 BIO

下面是传统BIO的模型图,为 一请求一应答。

可见其对于一个客户端就有一个线程。

ä¼ ç»ŸBIO通信模型图

采用 BIO通信模型 的 Server, 如上图所示,有一个独立的 Acceptor 线程来监听客户端的连接。 一般, 在 while(true) 循环之中,服务端会调用 accept() 方法来等待接收客户端的连接。 请求一旦接收到一个连接,就可以建立 通信socket ,并且在这个 socket 上面进行操作。这个时候, 无法接受其他的客户端的连接请求, 只能等待当前链接的客户端的操作完成。 如果想要对多个客户端支持连接,可以通过多线程的方式。

我们上面提到了多线程,必须使用多线程的原因是 socket.accept()socket.read(),socket.write()这三个函数都是同步阻塞的。那么多线程有两种方式:

  1. 最基本的方式就是,每次接收到 client 的连接请求之后,就要为这个 client 创建 一个线程进行处理,处理完成 之后,再通过 Output Stream 来返回给客户端,再将这个新创建的线程销毁。
  2. 既然多线程,那么第一个想到的就是 线程池机制, 这样创建和回收线程的成本都比较低。使用 FixedThreadPool ,实现线程数量的控制。

要是 client 并发访问量增加之后,第一种方式会出现什么问题?

之前总结过的,在 JVM 之中,线程是非常宝贵的资源。线程的创建成本和销毁成本都是重量级的。那么并发访问量急剧增加,可能会导致:

  • 堆栈溢出
  • 创建新线程失败

等等问题,最后导致宕机。

16.2.2 伪异步 IO

为了解决之前所说的,同步阻塞I/O 面临的链路需要线程处理的问题,有人采用了我们上面讲到过的第二种方法进行了优化:后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M, 线程池最大线程数N的比例关系。其中的M可以远远大于N(我们上面提到过,可以使用 FixedThreadPool 来实现线程数量的控制,这样可以避免海量并发接入导致线程耗尽)

下面是模型图:

伪异步IO模型图

原理是:

采用线程池和任务队列,可以实现一种叫做 伪异步 的 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 模型之中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的 Socket 实现, 两种通道都支持 阻塞 和 非阻塞 两种模式。 阻塞模式和传统之中的支持一样,比较简单,但是性能和可靠性都不好。 非阻塞模型就与其相反。

16.3.2 NIO 的特性,NIO和IO的区别

总结: NIO 流是非阻塞 IO, 但是 IO 流是阻塞 IO。

  1. Non-Blocking IO(非阻塞IO)

    IO 流是阻塞的, NIO 流是不阻塞的。

    Java NIO 使我们可以进行非阻塞IO操作,比如,单线程之中从通道读取数据到 buffer, 同时进行其他任务的处理。当数据被读取到 buffer 之中后,线程再继续处理数据。 写数据也是一样的。另外,非阻塞写 也是如此,一个线程请求写入一些数据到某通道,但是不需要等待其完全写入,线程同时可以去做其他的事情。

    Java IO 的各种流是阻塞的,这意味着,如果一个线程使用 read() 或者 Write()的时候,线程就会被阻塞,直到数据被读取或者写入。在这个期间,该线程就不能做任何事情了。

  2. Buffer(缓冲区)

    IO面向流(Stream Oriented),而 NIO 面向缓冲区(Buffer oriented)

    Buffer 是一个对象,其包含一些要写入或者要读出的数据。

    在面向流的 I/O (Stream Oriented) 之中,可以将数据直接写入或者读到 Stream 对象之中。虽然 Stream 之中也可以有 Buffer 开头的扩展类,但是其只是流的包装类,还是从流到缓冲区。但是 NIO 却是直接读到 Buffer 之中进行操作。

    在 NIO 库之中,所有数据都是用缓冲区处理的。

    最常用的缓冲区是 ByteBuffer, 一个 ByteBuffer 提供一组功能用于操作 Byte 数组。除了 ByteBuffer, 还有其他的一些缓冲区,事实上,每一种 Java 基本类型,除了 Boolean, 都有一种对应的缓冲区。

  3. Channel (通道)

    NIO 通过 Channel 来进行读写。

    通道是双向的,可读,也可写。而流的处理是单向的。无论读写,通道只能和 Buffer 交互,因为 Buffer, 通道可以异步的读写。

  4. Selector (选择器)

    NIO 有选择器,但是 IO 没有。

    选择器用于使用单个线程来处理多个通道。Selector 用来提高系统的效率。

一个单线程中Selector维护3个Channel的示意图

16.3.3 NIO读数据和写数据方式

通常而言,NIO 之中的所有 IO 都是从 Channel 开始的。

  • 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据
  • 从通道进行数据写入:创建一个缓冲区, 填充数据,并且要求通道写入数据。

下面是具体的操作图示:

NIO读写数据的方式

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(): 写入文件