- Java 基础知识
- JDK/JRE/JVM 三者之间的联系与区别
JDK: 开发者提供的开发工具箱,是给程序开发者用的。它包括完整的JRE(Java Runtime Environment),Java运行环境,还包含了其他供开发者使用的工具包。
JRE: Java Runtime Environment jvm运行时所必须的包依赖的环境都在jre中
JVM: 当我们运行一个程序时,JVM 负责将字节码转换为特定机器代码,JVM 提供了内存管理/垃圾回收和安全机制等。这种独立于硬件和操作系统,正是 java 程序可以一次编写多处执行的原因。
JDK > JRE > JVM
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
|
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(以后介绍)。
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点: 没有面向对象易维护、易复用、易扩展
优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点: 性能比面向过程低
-
- Java基本数据类型
| byte | short | int | long | double | float | char | boolean |
字节大小 | 1 | 2 | 4 | 8 | 8 | 4 | 2 | 1 |
占位大小 | 8 | 16 | 32 | 64 | 64 | 32 | 16 | 8 |
- 名字与类名相同;
- 没有返回值,但不能用void声明构造函数;
- 生成类的对象时自动执行,无需调用。
-
- 构造器 Constructor是否可被override
-
父类的私有属性和构造方法并不能被继承,所以Constructor 也就不能被override(重写),但是可以overload(重载),所以你可以看到一个类中有多个构造函数的情况。
1. 父类有无参构造器,子类才可以写无参构造器;父类有含参构造器,子类才可以写含参构造器
2. 构造器不能被继承、重写
3. 当进行无参构造时,先调用父类无参构造器,然后调用子类无参构造器;当进行含参构造时,先调用父类含参构造器,然后调用子类含参构造器。
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
-
- this和super 关键字
- super关键字用于从子类访问父类的变量和方法.也包含构造方法
- this关键字用于引用类的当前实例。此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。this也可以调用当前类的构造方法。
- super 调用父类中的其他构造方法时,调用时要放在构造方法的首行!this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。因为被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西。
简单的来说:String 类中使用 final 关键字字符数组保存字符串, private final char value[] ,所以 String
对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder
中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用
StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
1. 操作少量的数据= String
2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
-
- hashCode与equals和==
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
我们以“HashSet如何检查重复”为例子来说明为什么要有hashCode:
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
-
-
- hashCode()与equals()的相关规定
-
1. 如果两个对象相等,则hashcode一定也是相同的
2. 两个对象相等,对两个对象分别调用equals方法都返回true
3. 两个对象有相同的hashcode值,它们也不一定是相等的
4. 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
-
-
- 为什么两个对象有相同的hashcode值,它们也不一定是相等的?
-
因为hashCode() 所使用的杂凑算法[杂凑运算又称hash函数,Hash函数(也称杂凑函数或杂凑算法)就是把任意长的输入消息串变化成固定长的输出串的一种函数]也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用equals()来判断
是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
-
-
- ==与equals
-
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的equals 方法比较的是对象的值。当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法
2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
3. 一个类可以实现多个接口,但最多只能实现一个抽象类
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
-
- static 关键字
static 关键字主要有以下四种使用场景:
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
调用格式:
类名.静态变量名
类名.静态方法名()
静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
静态代码块的格式是:static { 语句体; }
一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问.
静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
-
-
- 静态内部类(static修饰类的话只能修饰内部类)
-
静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:
1. 它的创建是不需要依赖外围类的创建。
2. 它不能使用任何外围类的非static成员变量和方法。
-
-
- 静态导包(用来导入类中的静态资源,1.5之后的新特性)
-
格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
总结:
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
static{}静态代码块与{}非静态代码块(构造代码块)
相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。
不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
-
-
- Java中static的执行顺序问题
-
父的静态内容à子的静态内容à父的非静态代码块(如果有)à父的构造方法à子的非静态代码块(如果有)à子的构造方法
总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法
public class staticDemo { static { int x=5; System.out.println("父亲的静态代码块执行了"); } static { int x=5; System.out.println("父亲的静态代码块2执行了"); } static void print(){ System.out.println("父亲的静态方法代码块执行了"); } public staticDemo(){ System.out.println("父亲的构造方法代码块执行了"); } public static void main(String[] args) { staticDemo.print(); // staticDemo sd=new staticDemo(); // sd.print(); System.out.println("父亲的主方法代码块执行了"); } { System.out.println("父亲的非静态代码块执行了"); } } class sonStatic extends staticDemo{ static { System.out.println("儿子的静态代码块执行"); } public sonStatic(){ System.out.println("儿子的构造函数块执行"); } { System.out.println("儿子的非静态代码块执行"); } static void printf(){ System.out.println("儿子的静态方法执行"); } public static void main(String args[]){ sonStatic.printf(); sonStatic s=new sonStatic(); } } |
输出:
父亲的静态代码块执行了
父亲的静态代码块2执行了
儿子的静态代码块执行
儿子的静态方法执行
父亲的非静态代码块执行了
父亲的构造方法代码块执行了
儿子的非静态代码块执行
儿子的构造函数块执行
/** * 三要素: * 1、只创建一次, * 2、保存 * 3、提供接口 */ public class Singleton01 { public static final Singleton01 INSTANCE = new Singleton01(); private Singleton01(){ } }
|
优点:基于classloader机制避免了多线程同步问题
没有加锁,执行效率高
缺点:类加载的时候就初始化了,没有达到lazyloading
的效果,浪费空间
public enum Singleton02 { INSTANCE } class Singleton02Test{ public static void main(String[] args) { Singleton02 instance = Singleton02.INSTANCE; } }
|
它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
public class Singleton03 { private static final Singleton03 INSTANCE; static { try { //可以写一些业务逻辑,比如加载jdbc的配置文件等 Properties properties = new Properties(); properties.load(Singleton03.class.getClassLoader().getResourceAsStream("jdbc.properties")); String username = properties.getProperty("username"); System.out.println(username); } catch (IOException e) { e.printStackTrace(); } INSTANCE= new Singleton03(); } private Singleton03 (){ } public static final Singleton03 getInstance() { return INSTANCE; } }
|
public class Singleton04 { private static Singleton04 instance; private Singleton04 (){} public static Singleton04 getInstance() { if (instance == null) { instance = new Singleton04(); } return instance; } }
|
线程安全性测试
public class TestSingleton4 { public static void main(String[] args) throws InterruptedException, ExecutionException { Singleton04 s1 = Singleton04.getInstance(); Singleton04 s2 = Singleton04.getInstance(); System.out.println(s1 == s2); Callable<Singleton04> c = new Callable<Singleton04>() { @Override public Singleton04 call() throws Exception { return Singleton04.getInstance(); } }; ExecutorService es = Executors.newFixedThreadPool(2); Future<Singleton04> f1 = es.submit(c); Future<Singleton04> f2 = es.submit(c); Singleton04 s1 = f1.get(); Singleton04 s2 = f2.get(); System.out.println(s1 == s2); System.out.println(s1); System.out.println(s2); es.shutdown(); } }
|
这种方式不支持多线程。因为没有加锁 synchronized。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作
public class Singleton05 { private static Singleton04 instance; private Singleton05 (){} public static synchronized Singleton05 getInstance() { if (instance == null) { instance = new Singleton04(); } return instance; } }
|
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
public class Singleton06 { private Singleton06(){ } private static class Inner{ private static final Singleton06 INSTANCE = new Singleton06(); } public static Singleton06 getInstance(){ double f = 3.14; return Inner.INSTANCE; } }
|
在内部类被加载和初始化时,才创建实例,对象静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
集合类存放于Java.util 包中,主要有3种:set(集)、list(列表包含 Queue)和map(映射)。
- Collection:Collection 是集合 List、Set、Queue 的最基本的接口。
- Iterable:迭代器,可以通过迭代器遍历集合中的数据
- Map:是映射表的基础接口
-
- List接口
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类: 分别是 ArrayList、Vector 和 LinkedList。
-
-
- ArrayList(数组)
-
- ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数 组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进 行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
- ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
- ArrayList默认容量是10,如果初始化时一开始指定了容量,或者通过集合作为元素,则容量为指定的大小或参数集合的大小。每次扩容为原来的1.5倍,如果新增后超过这个容量,则容量为新增后所需的最小容量。如果增加0.5倍后的新容量超过限制的容量,则用所需的最小容量与限制的容量进行判断,超过则指定为Integer的最大值,否则指定为限制容量大小。然后通过数组的复制将原数据复制到一个更大(新的容量大小)的数组。
-
- Vector(数组实现、线程同步)
-
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此, 访问它比访问 ArrayList 慢。
-
-
- LinkList(链表)
-
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较 慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆 栈、队列和双向队列使用。
LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: java List list=Collections.synchronizedList(new LinkedList(...));
-
-
-
- 内部结构
-
-
-
-
- Arraylist与LinkedList异同
-
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之
前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:);
3. 插入和删除是否受元素位置的影响:
① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
5. 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
-
-
- ArrayList与Vector 区别
-
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。【CopyOnWriteArrayList是同步的】。
-
-
- System.arraycopy()和Arrays.copyOf()
-
看两者源代码可以发现`copyOf()`内部调用了`System.arraycopy()`方法
区别:
1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
2.copyOf()是系统自动在内部新建一个数组,并返回该数组。
-
- Set接口
HashSet中的数据是无序的不可重复的。HashSet按照哈希算法存取数据的,具有非常好性能,它的工作原理是这样的,当向HashSet中插入数据的时候,他会调用对象的hashCode得到该对象的哈希码,然后根据哈希码计算出该对象插入到集合中的位置。
-
-
- hashCode() 与 equals() 的相关规定
-
1. 如果两个对象相等,则 hashcode 一定也是相同的
2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true
3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
hashCode方法:
该方法是Object中定义的方法,返回int类型,在Object类中的缺省实现是:该方法执行结束之后的返回值可以“等同”看作是一个java对象的内存地址。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object中默认情况下,只要是new的新对象,内存地址就不同,hashCode方法的执行结果也不同。
Object中是这样编写hashCode方法的:
public native int hashCode();
注意:该方法通常用来将对象的 内存地址 转换为整数之后返回。虽然这个方法以分号结束,但不是一个抽象方法,方法修饰符列表中有native关键字,这种方法调用的是底层的C++程序。这属于JNI技术【Java Native Interface】,这属于异构系统整合技术
-
-
-
- equals和hashCode举例说明
-
-
- HashSetTest02
import java.util.*;
public class HashSetTest02 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
Set set = new HashSet(); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); }
System.out.println("p1.hashCode=" + p1.hashCode()); System.out.println("p2.hashCode=" + p2.hashCode()); System.out.println("p3.hashCode=" + p3.hashCode()); } }
class Person {
String name;
int age; } |
加入了重复的数据,因为hashCode是不同的,所以会根据算出不同的位置,存储格式
对象 | Person{张三,20} | Person{李四,30} | Person{张三,40} |
hashCode值 | 7699183 | 14285251 | 10267414 |
- HashSetTest03进一步完善,覆盖equals
import java.util.*;
public class HashSetTest03 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
System.out.println("p1 equals p2," + p1.equals(p2)); System.out.println("p1 equals p3," + p1.equals(p3));
Set set = new HashSet(); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); }
System.out.println("p1.hashCode=" + p1.hashCode()); System.out.println("p2.hashCode=" + p2.hashCode()); System.out.println("p3.hashCode=" + p3.hashCode()); } }
class Person {
String name;
int age;
//覆盖equals public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person)obj; return this.name.equals(p.name); } return false;
} } |
以上仍然存在重复数据,在Person中覆盖了hashCode方法,能够正确的比较出两个Person是相等的还是不等的,但是为什么HashSet中还是放入了重复数据?因为Person对象的hashCode不同,所以它就换算出了不同的位置,让后就会把相关的值放到不同的位置上,就忽略equlas,所以我们必须覆盖hashCode方法
Person{张三,20} | Person{李四,30} | Person{张三,40} |
7699183 | 14285251 | 10267414 |
- HashSetTest04,只覆盖hashCode,不覆盖equals
import java.util.*;
public class HashSetTest04 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
System.out.println("p1 equals p2," + p1.equals(p2)); System.out.println("p1 equals p3," + p1.equals(p3));
Set set = new HashSet(); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); }
System.out.println("p1.hashCode=" + p1.hashCode()); System.out.println("p2.hashCode=" + p2.hashCode()); System.out.println("p3.hashCode=" + p3.hashCode()); } }
class Person {
String name;
int age;
//覆盖hashCode public int hashCode() { return (name==null)?0:name.hashCode(); } } |
以上示例,张三的hashCode相同,当两个对象的equals不同,所以认为值是不一样的,那么java会随机换算出一个新的位置,放重复数据
Person{张三,20} | Person{李四,30} | Person{张三,40} |
774889-1 | 14285251 | 774889-2 |
- HashSetTest05,覆盖equals,覆盖hashCode
import java.util.*;
public class HashSetTest05 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
System.out.println("p1 equals p2," + p1.equals(p2)); System.out.println("p1 equals p3," + p1.equals(p3));
Set set = new HashSet(); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); }
System.out.println("p1.hashCode=" + p1.hashCode()); System.out.println("p2.hashCode=" + p2.hashCode()); System.out.println("p3.hashCode=" + p3.hashCode()); } }
class Person {
String name;
int age;
//覆盖hashCode public int hashCode() { return (name==null)?0:name.hashCode(); }
//覆盖equals public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person)obj; return this.name.equals(p.name); } return false;
}
} |
以上输出完全正确的,因为覆盖了equals和hashCode,当hashCode相同,它会调用equals进行比较,如果equals比较相等将不加把此元素加入到Set中,但equals比较不相等会重新根据hashCode换算位置仍然会将该元素加入进去的。
Person{张三,20} | Person{李四,30} |
|
774889 | 842061 |
|
再次强调:特别是向HashSet或HashMap中加入数据时必须同时覆盖equals和hashCode方法,应该养成一种习惯覆盖equals的同时最好同时覆盖hashCode
Java要求:
两个对象equals相等,那么它的hashcode相等
两个对象equals不相等,那么它的hashcode并不要求它不相等,但一般建议不相等
hashcode相等不代表两个对象相等(采用equals比较)
TreeSet可以对Set集合进行排序,默认自然排序(即升序),但也可以做客户化的排序。
基本类型的包装类和String他们都是可以排序的,他们都实现Comparable接口,但是自定义的引用数据类型如果需要排序的话需要是一个可排序的类才行
-
-
-
- 实现Comparable接口完成排序
-
-
- class Person implements Comparable对Person自然排序
import java.util.*;
public class TreeSetTest04 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Set set = new TreeSet(); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); } } }
class Person implements Comparable {
String name;
int age;
//如果覆盖了equals,最好保证equals和compareto在相等情况下的比较规则是一致的 public int compareTo(Object o) { if (o instanceof Person) { Person p = (Person)o; //升序 //return (this.age - p.age); //降序 return (p.age-this.age); } throw new IllegalArgumentException("非法参数,o=" + o); } } |
-
-
-
- 实现Comparator接口完成排序
-
-
- class PersonComparator implements Comparator
import java.util.*;
public class TreeSetTest05 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30;
Comparator personComparator = new PersonComparator();
Set set = new TreeSet(personComparator); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); } } }
class Person {
String name;
int age; }
//实现Person的比较器 //Comparator和Comparable的区别? //Comparable是默认的比较接口,Comparable和需要比较的对象紧密结合到一起了 //Comparator可以分离比较规则,所以它更具灵活性 class PersonComparator implements Comparator {
public int compare(Object o1, Object o2) { if (!(o1 instanceof Person)) { throw new IllegalArgumentException("非法参数,o1=" + o1); } if (!(o2 instanceof Person)) { throw new IllegalArgumentException("非法参数,o2=" + o2); } Person p1 = (Person)o1; Person p2 = (Person)o2; return p1.age - p2.age; } } |
- TreeSetTest06
import java.util.*;
public class TreeSetTest06 {
public static void main(String[] args) { Person p1 = new Person(); p1.name = "张三"; p1.age = 20;
Person p3 = new Person(); p3.name = "张三"; p3.age = 40;
Person p2 = new Person(); p2.name = "李四"; p2.age = 30; //采用匿名类实现比较器 Set set = new TreeSet(new Comparator() { public int compare(Object o1, Object o2) { if (!(o1 instanceof Person)) { throw new IllegalArgumentException("非法参数,o1=" + o1); } if (!(o2 instanceof Person)) { throw new IllegalArgumentException("非法参数,o2=" + o2); } Person p1 = (Person)o1; Person p2 = (Person)o2; return p1.age - p2.age; } }); set.add(p1); set.add(p2); set.add(p3);
for (Iterator iter=set.iterator(); iter.hasNext();) { Person p = (Person)iter.next(); System.out.println("name=" + p.name + ", age=" + p.age); } } }
class Person {
String name;
int age; } |
-
-
-
- Comparable和Comparator的区别?
-
-
一个类实现了Camparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
1、类的没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
2、可以使用多种排序标准,比如升序、降序等
- 将自定义对象放到List或其他集合中如何排序?实现comparable接口,重写compareTo方法 或者 单独写一个比较器comparator
- Comparable(比较规则固定,比较规则写在类的内部)和Comparator(比较规则经常变化,写在比较器中,符合OCP开发原则)的差别? Comparator体现了哪个设计模式?策略模式
-
-
- HashMap(数组+链表+红黑树)
-
HashMap 根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。我们用下面这张图来介绍 HashMap 的结构。
-
-
-
- Java7的实现
-
-
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry的实例,Entry包含四个属性:key, value, hash值和用于单向链表的next。
- capacity:当前数组容量,始终保持2^n,可以扩容,扩容后数组大小为当前的2倍。
- loadFactor:负载因子,默认为0.75。
- threshold:扩容的阈值,等于capacity * loadFactor
-
-
- Java8的实现
-
-
Java8对HashMap进行了一些修改,最大的不同就是利用了红黑树,所以其由数组+链表+红黑树组成。
根据Java7 HashMap的介绍,我们知道,查找的时候,根据hash值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低这部分的开销,在Java8中,当链表中的元素超过了8个以后, 会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。
Jdk1.7和Jdk1.8最大的不同就是树化,简单地解释一下树化的过程
如果在创建 HashMap 实例时没有给定capacity、loadFactor则默认值分别是16和0.75。
当好多bin被映射到同一个桶时,如果这个桶中bin的数量小于等于TREEIFY_THRESHOLD(默认是8)当然不会转化成树形结构存储;如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD ,但是capacity小于MIN_TREEIFY_CAPACITY(默认是64) 则依然使用链表结构进行存储,此时会对HashMap进行扩容;如果capacity大于了MIN_TREEIFY_CAPACITY ,才有资格进行树化(当bin的个数大于8时)。
-
-
- HashTable(线程安全)
-
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如 ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用 ConcurrentHashMap替换。
-
-
- TreeMap(可排序)
-
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
如果使用排序的映射,建议使用TreeMap。
在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
参考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html
-
-
- HashMap 和 Hashtable 的区别
-
1. 线程是否安全: HashMap是非线程安全的,HashTable是线程安全的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. 效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它;
3. 对Null key和Null value的支持: HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。但是在HashTable中put 进的键值只要有一个null,直接抛出NullPointerException。
4. 初始容量大小和每次扩充容量大小的不同:
①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap 会将其扩充为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
-
-
- HashMap 的长度为什么是2的幂次方
-
为了能让HashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采
用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
-
-
- HashSet 和 HashMap 区别
-
如果你看过 HashSet 源码的话就应该知道:HashSet底层就是基于HashMap实现的。(HashSet 的源码非常非常少,因为除了clone()方法、writeObject()方法、readObject()方法是HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。)HashSet就是HashMap的key
-
- Collections 工具类常用方法:
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
-
-
- 查找,替换操作
-
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。
int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。
int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定list中所有元素。 int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,
int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
Collections提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。 synchronizedList(List<T> list) //返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定set支持的同步(线程安全的)set。
-
-
- Collections设置不可变集合
-
emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 singletonXxx():返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。
unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。
-
- Arrays类的常见操作
排序 : sort()
查找 : binarySearch()
比较: equals()
填充 : fill()
转列表: asList()
转字符串 : toString()
复制: copyOf()
- JAVA 异常分类及处理
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下 会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用 这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
Throwable 是Java语言中所有错误或异常的超类。下一层分为Error和Exception
- Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果 出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception(RuntimeException、CheckedException)
- Exception又有两个分支,一个是运行时异常RuntimeException,一个是 CheckedException。
RuntimeException 如:NullPointerException、ClassCastException:一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。RuntimeException是那些可能在Java虚拟机正常运行期间抛出的异常的超类。 如果出现RuntimeException,那么一定是程序员的错误.
检查异常 CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强 制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行try catch,该类异常一般包括几个方面:
遇到问题不进行具体处理,而是继续抛给调用者(throw,throws)
抛出异常有三种形式,一是throw,一个throws,还有一种系统自动抛异常
try catch 捕获异常针对性处理方式
-
-
- Throw和throws的区别:
-
1. throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的 是异常对象。
2. throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方 式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并 将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语 句,因为执行不到。
3. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常, 执行 throw 则一定抛出了某种异常对象。
4.两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异 常,真正的处理异常由函数的上层调用处理。
文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流,组成文件的字符序列称为字符流。Java中根据流的方向可以分为输入流和输出流。输入流是将文件或其它输入设备的数据加载到内存的过程;输出流恰恰相反,是将内存中的数据保存到文件或其他输出设备,详见下图:
文件或其他输入设备(键盘) |
内存(Java程序) |
输入流 |
文件或其他输入设备(控制台) |
输出流 |
文件是由字符或字节构成,那么将文件加载到内存或再将文件输出到文件,需要有输入和输出流的支持,那么在Java语言中又把输入和输出流分为了两个,字节输入和输出流,字符输入和输出流,见下表:
输入流 |
字节输入流 |
字符输入流 |
InputStream |
Reader |
输出流 |
字节输出流 |
字符输出流 |
OutputStream |
Writer |
-
-
- InputStream(字节输入流)
-
InputStream是字节输入流,InputStream是一个抽象类,所有继承了InputStream的类都是字节输入流,主要了解以下子类即可:
主要方法介绍:
方法 | 描述 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源。 |
abstract int read() | 从输入流读取下一个数据字节 |
int read(byte b[]) | 从输入流中读取一定数量的字节并将其存储在缓冲区数组 b 中。 |
int read(byte b[], int off, int len)
| 将输入流中最多 len 个数据字节读入字节数组。 |
-
-
- OutputStream(字节输出流)
-
所有继承了OutputStream都是字节输出流
主要方法介绍
方法 | 介绍 |
public void close()
| 关闭此输出流并释放与此流有关的所有系统资源。 |
void flush()
| 刷新此输出流并强制写出所有缓冲的输出字节。 |
void write(byte b[])
| 将 b.length 个字节从指定的字节数组写入此输出流。 |
void write(byte b[], int off, int len)
| 将指定字节数组中从偏移量 off 开始的 len 个字节写入此输出流。 |
abstract void write(int b) | 将指定的字节写入此输出流 |
-
-
- Reader(字符输入流)
-
所有继承了Reader都是字符输如流
主要方法介绍
方法 | 介绍 |
abstract public void close() | 关闭该流 |
public int read() | 读取单个字符 |
int read(char cbuf[]) | 将字符读入数组 |
abstract public int read(char cbuf[], int off, int len) | 将字符读入数组的某一部分 |
-
-
- Writer(字符输出流)
-
所有继承了Writer都是字符输出流
主要方法介绍
方法 | 介绍 |
Writer append(char c) | 将指定字符追加到此 writer |
abstract public void close() | 关闭此流,但要先刷新它 |
abstract public void flush() | 刷新此流 |
write(char cbuf[]) | 写入字符数组 |
abstract public void write(char cbuf[], int off, int len) | 写入字符数组的某一部分 |
void write(int c) | 写入单个字符 |
void write(String str) | 写入字符串 |
void write(String str, int off,int len) | 写入字符串的某一部分 |
- BIO和NIO
IO模型就是用什么样的通道进行数据的发送和接受,很大程度上决定了程序通信的性能。Java支持BIO、NIO、AIO三种网络编程模型IO模式
BIO:同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。但是连接上并不是时刻都有IO操作的这样造成了不必要的线程开销。而且读写数据时阻塞的。适用于连接数目比较小且固定的架构,这种方式对服务器资源要去较高并发局限于应用中,但是程序简单易理解
NIO:同步非阻塞,服务器实现模式一个线程处理多个请求(连接),即看客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理;适用于连接数目多且连接比较短的架构。
AIO:异步非阻塞,AIO引入异步通道的概念,采用Proactor模式,简化了程序编写,有效的请求才启动线程,他的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般使用与连续数较多且连接时间较长的应用,
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
-
-
- 传统 BIO
-
BIO通信(一请求一应答)
采用 BIO通信模型 的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。
如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()、socket.read()、socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。
public static void main (String[] args) { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(7777); System.out.println("服务已启动,端口号是::"+7777); while (true){ Socket socket = serverSocket.accept(); new Thread(){ @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(),true); String expression; while (true){ if((expression = in.readLine())==null) break;; System.out.println("服务器端收到的数据时::"+expression); out.print("Server reviced ::" +expression); } }catch (Exception e){ e.printStackTrace(); } } }.start(); } } catch (IOException e) { e.printStackTrace(); }finally { //关闭流和socket } } |
使用dos 窗口作为客户端进行测试 telnet 127.0.0.1 7777 --àctrl+]--à回车输入要发送的消息
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
伪异步IO模型图
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型。
public static void main(String[] args) throws IOException { ExecutorService pool = Executors.newCachedThreadPool(); System.out.println("tcp服务端启动啦……"); ServerSocket serverSocket = new ServerSocket(7777); //伪异步的实现,通过多线程 while(true) { // 也会阻塞在这里,等待连接 Socket socket = serverSocket.accept(); pool.execute(new Runnable() { @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(),true); String expression; while (true){ if((expression = in.readLine())==null) break;; System.out.println("服务器端收到的数据时::"+expression); out.print("Server reviced ::" +expression); } }catch (Exception e){ e.printStackTrace(); } } }); } } |
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
-
- NIO(Non-Blocking I/O)
NIO包含下面几个核心的组件:
- Channels
- Buffers
- Selectors
整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。
-
-
- NIO简介
-
Java NIO是java 1.4 之后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用是,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可读之前,该线程可以继续做其他的事项,非阻塞也是如此
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。随着JDK 7的推出,NIO系统得到了扩展,为文件系统功能和文件处理提供了增强的支持。 由于NIO文件类支持的这些新的功能,NIO被广泛应用于文件处理。
JavaNIO Buffers用于和NIOChannel交互。我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels.
Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
在Java NIO中使用的核心缓冲区如下(覆盖了通过I/O发送的基本数据类型:byte, char、short, int, long, float, double ,long):
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- FloatBuffer
- DoubleBuffer
- LongBuffer
-
-
-
- Buffer的常用属性
-
-
任何形式的Buffer底层都是一个数组。一个Buffer有三个属性是必须掌握的,position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。
- capacity容量:数组的长度
作为一块内存,buffer有一个固定的大小,叫做capacit(容量)。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。
- position位置:当前数组的下标的指针
- 当写入数据到Buffer的时候需要从一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1。
- 当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。
- limit限制:
- 写模式,limit的含义是我们所能写入的最大数据量,它等同于buffer的容量。
- 读模式,limit则代表我们所能读取的最大数据量(可以理解为底层数组已经填充了多少单元格),他的值等同于写模式下position的位置。换句话说,您可以读取与写入数量相同的字节数(限制设置为写入的字节数,由位置标记)。
属性 | 描述 |
Capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记 |
-
-
-
- Buffer的常用api
-
-
public abstract class Buffer { //JDK1.4时,引入的api public final int capacity( )//返回此缓冲区的容量 public final int position( )//返回此缓冲区的位置 public final Buffer position (int newPositio)//设置此缓冲区的位置 public final int limit( )//返回此缓冲区的限制 public final Buffer limit (int newLimit)//设置此缓冲区的限制 public final Buffer mark( )//在此缓冲区的位置设置标记 public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置 public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖 public final Buffer flip( )//反转此缓冲区,读写模式的反转 public final Buffer rewind( )//重绕此缓冲区 public final int remaining( )//返回当前位置与限制之间的元素数 public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素 public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组 public abstract Object array();//返回此缓冲区的底层实现数组 public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量 public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区 } |
Buffer读写数据,通常遵循四个步骤
- 把数据写入buffer;
- 调用flip;
- 从Buffer中读取数据;
- 调用buffer.clear()或者buffer.compact()。
当写入数据到buffer中时,buffer会记录已经写入的数据大小。当需要读数据时,通过flip()方法把buffer从写模式调整为读模式;在读模式下,可以读取所有已经写入的数据。
当读取完数据后,需要清空buffer,以满足后续写入操作。清空buffer有两种方式:调用clear() 或compact()方法。clear会清空整个buffer,compact则只清空已读取的数据,未被读取的数据会被移动到buffer的开始位置,写入位置则近跟着未读数据之后。
-
-
-
- ByteBuffer常用api
-
-
八种基本数据类型,出来boolean以外,每个都有个buffer类型与之相对应,最常用的buffer就是ByteBuffer了,该类主要有一下方法
public abstract class ByteBuffer { //缓冲区创建相关api public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区 public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量 public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用 //构造初始化位置offset和上界length的缓冲区 public static ByteBuffer wrap(byte[] array,int offset, int length) //缓存区存取相关API public abstract byte get( );//从当前位置position上get,get之后,position会自动+1 public abstract byte get (int index);//从绝对位置get public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1 public abstract ByteBuffer put (int index, byte b);//从绝对位置上put }
|
- 在Java NIO中,主要使用的通道如下(涵盖了UDP 和 TCP 网络IO,以及文件IO):
- FileChannel:主要用来对本地文件进行 IO 操作,常见的方法有
|
- DatagramChannel: 用于UDP的数据读写
- SocketChannel: 用于TCP的数据读写,客户端实现
- ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,服务器实现
- Java NIO Channel通道和流区别
- 通道可以读也可以写,流一般来说是单向的【只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流】。
- 通道可以异步读写。
- 通道总是基于缓冲区Buffer来读写。
Java NIO提供了Selector 一般称为选择器 ,当然也可以翻译为多路复用器。这是一个可以用于监视多个通道
的对象,如数据到达,连接打开等。因此,单线程可以监视多个通道中的数据。
如果应用程序有多个通道(连接)打开,但每个连接的流量都很低,则可考虑使用它。 例如:在聊天服务器中。
要使用Selector的话,我们必须把Channel注册到Selector上,然后就可以调用Selector的select()方法。这个方法会进入阻塞,直到有一个channel的状态符合条件。当方法返回后,线程可以处理这些事件。
使用Selector的好处在于:使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。
-
-
-
- Selector
-
-
Selector是一个抽象类,定义了一些主要的方法。有一个具体的实现SelectorImpl
public abstract class Selector implements Closeable { public static Selector open();//得到一个选择器对象 public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间 public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey
}
|
selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
selector.wakeup();//唤醒selector
selector.selectNow();//不阻塞,立马返还
-
- BIO NIO AIO 对比
- Channels and Buffers(通道和缓冲区)
- BIO NIO AIO 对比
IO是面向流的,NIO是面向缓冲区的
- 标准的IO编程接口是面向字节流和字符流的。而NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer缓冲区写入到通道中;( NIO中的所有I/O操作都是通过一个通道开始的。)
- Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;
- Java NIO是面向缓存的I/O方法。 将数据读入缓冲器,使用通道进一步处理数据。 在NIO中,使用通道和缓冲区来处理I/O操作。
-
- Non-blocking IO(非阻塞IO)
-
IO流是阻塞的,NIO流是不阻塞的。
- Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
-
- Selectors(选择器)
-
NIO有选择器,而IO没有。
- 选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。
- 线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
通常来说NIO中的所有IO都是从Channel通道开始的。
从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。数据读取和写入操作图示:
- BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应java.nio包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在Java 7中引入了NIO的改进版NIO 2,它是异步非阻塞的IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说AIO的应用还不是很广泛,Netty之前也尝试使用过AIO,不过又放弃了。
| BIO | NIO | AIO |
IO 模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |