面向对象基本特性介绍

面向对象OOP是基于面向过程而言,面向对象简单来说就是将功能封装到对象(数据和操作结合)里,我们面向对象,让对象去完成这些功能。万物皆对象。
三大特性是继承、封装与多态。

多态实现原理

Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用(invokevitual)和接口引用调用(invokeinterface)的实现则有所不同。

方法表

方法表是实现动态调用的核心。上面讲过方法表存放在方法区中的类型信息中。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表,方法表中的每一个项都是对应方法的指针。这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。

Java基本类型

基本类型 字节
byte 1字节
short 2字节
int 4字节
long 8字节
float 4字节
double 8字节
boolean 1字节或4字节(被编译为int)
char 2字节

范围:拿char举例,char的范围是[-2^15-2^15-1]。

JDK、JRE和JVM

image-20200212172253575.png

常用包

包名 说明
java.lang 该包提供了Java编程的基础类,例如 Object、Math、String、StringBuffer、System、Thread等,不使用该包就很难编写Java代码了。
java.util 该包提供了包含集合框架、遗留的集合类、事件模型、日期和时间实施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组)。
java.io 该包通过文件系统、数据流和序列化提供系统的输入与输出。
java.net 该包提供实现网络应用与开发的类。
java.sql 该包提供了使用Java语言访问并处理存储在数据源(通常是一个关系型数据库)中的数据API。
java.awt/javax.swing 这两个包提供了GUI设计与开发的类。java.awt包提供了创建界面和绘制图形图像的所有类,而javax.swing包提供了一组“轻量级”的组件,尽量让这些组件在所有平台上的工作方式相同。
java.text 提供了与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。

重载与重写

区别 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)
  • 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

    内存泄漏的场景

    内存泄露是指不再使用的对象由于仍然被其他对象引用导致垃圾收集器不能及时释放这些对象占用的内存从而造成内存空间浪费的现象。

    引用类型

    | 级别 | 回收时机 | 用途 | 生存时间 | | —- | —- | —- | —- | | 强引用 | 从来不会 | 对象的一般状态 |
    | | 软引用 | 在内存不足的时候 | 联合 ReferenceQueue 构造有效期短/占内存大/生命周期长的对象的二级高速缓冲器(内存不足时才清空) | 内存不足时终止 | | 弱引用 | 在垃圾回收时 | 联合 ReferenceQueue 构造有效期短/占内存大/生命周期长的对象的一级高速缓冲器(系统发生GC则清空) | GC 运行后终止 | | 虚引用 | 在垃圾回收时 | 联合 ReferenceQueue 来跟踪对象被垃圾回收器回收的活动 | GC 运行后终止 |

场景

  • 使用静态的集合类静态的集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前容器中的对象不能被释放,会造成内存泄露。解决办法是最好不使用静态的集合类,如果使用的话,在不需要容器时要将其赋值为null。
  • 单例模式可能会造成内存泄漏单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。解决办法是单例对象中持有的其他对象使用弱引用,弱引用对象在GC线程工作时,其占用的内存会被回收掉。
  • 变量不合理的作用域如果变量的定义范围大于使用范围,并且在使用完后没有赋值为null的话,会出现内存泄露。定义变量的时候,能定义为局部变量就不要定义为成员变量,或者定义为成员变量的话,在使用完变量后,把变量赋值为null。
  • 数据库、网络、输入输出流,这些资源没有显示的关闭
  • 使用非静态内部类非静态内部类对象的构建依赖于其外部类,内部类对象会持有外部类对象的this引用,即时外部类对象不再被使用了,其占用的内存可能不会被GC回收,因为内部类的生命周期可能比外部类的生命周期要长,从而造成外部类对象不能被及时回收。解决办法是尽量使用静态内部类,静态内部类只是形式上在外部类的里面,静态内部类不会持有外部类的引用,可以把静态内部类理解成是一个独立的类,和外部类没什么关系。为什么非静态内部类持有对外部类的引用?非静态内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件,通过如下三个步骤,内部类对象通过this访问外部类对象的成员。

    1. 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同,这个成员变量就是指向外部类对象(this)的引用;
    2. 编译器自动为内部类的构造方法添加一个参数,参数的类型是外部类的类型,在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
    3. 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。

      序列化与反序列化

      序列化是指将Java对象转换为字节序列的过程,而反序列化则是将字节序列转换为Java对象的过程。

      实现序列化的好处

  • 实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(如:存储在文件里),实现永久保存对象。

  • 利用序列化实现远程通信,即:能够在网络上传输对象。

    实现原理

    只要对象实现了Serializable、Externalizable接口(该接口仅仅是一个标记接口,并不包含任何方法),则该对象就实现了序列化。

    序列化

    序列化,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内,这时调用writeObject()方法,即可将对象序列化,并将其发送给OutputStream(对象序列化是基于字节的,因此使用的InputStream和OutputStream继承的类)。

    反序列化

    反序列化,即反向进行序列化的过程,需要将一个InputStream封装在ObjectInputStream对象内,然后调用readObject()方法,获得一个对象引用(它是指向一个向上转型的Object),然后进行类型强制转换来得到该对象。

    序列化/反序列化失败场景

  • 声明为static和transient类型的成员变量不能被序列化

  • serialVersionUID 与对应的发送者的类的版本号不同不能被反序列化如果序列化的类未显式的声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。

    访问权限

    | 访问权限 | 本类 | 本包的类 | 子类 | 非子类的外包类 | | —- | —- | —- | —- | —- | | public | 是 | 是 | 是 | 是 | | protected | 是 | 是 | 是 | 否 | | default | 是 | 是 | 否 | 否 | | private | 是 | 否 | 否 | 否 |

&与&&的区别

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。
&的逻辑与和&&的短路与分别都是表示左边和右边都为true,结果才为true,但是使用短路与时,如果&&左边为false,那么整个值就直接判断为false,不会进行右边的逻辑操作。

Array和ArrayList的区别

  • Array可以包含基本类型和对象类型,ArrayList只能包含对象类型;
  • Array大小固定,ArrayList是动态变化的;
  • ArrayList有更多的特性,如addAll(),removeAll(),iterator()等等。

    Comparable和Comparator接口的作用以及区别

  • Comparable:只有compareTo();

  • Comparator:包含compare()+equals()。

    Collection和Collections的区别

  • Collection:集合类的上级接口,继承与他的接口主要有Set 和List;

  • Collections:针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

    快速失败(fail-fast)和安全失败(fail-safe)的区别

  • fail-fast:java.util包下面的所有的集合类都是快速失败的,快速失败的迭代器会抛出ConcurrentModificationException异常,具体底层是因为remove方法时,expectedModCount不等于modCount: final void checkForComodification() {
    if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
    }当用集合的方法进行add、remove、clear操作时,modCount就会发生改变,但是使用Iterator接口的remove()方法是不会改变的。

  • fail-safe:java.util.concurrent包下面的所有的类都是安全失败的,安全失败是基于对底层集合做拷贝,因此不会抛出异常。

    值传递和引用传递

  • 值传递是对基本变量而言的,传递的是变量的副本,改变副本不改变原值。

  • 引用传递是对于对象型变量而言的,传递的是对象的副本,对引用对象进行操作会改变源对象。

java中大部分都是值传递,包括引用对象,所谓的值传递个人理解是,传递了一个栈帧的拷贝,并且与源栈帧指向相同的句柄或者堆实例对象内存地址,以下是他人总结:

  1. 如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。
  2. 如果是对引用类型的数据进行操作,形参和实参保持指向同一个对象地址,引用不改变下,形参的操作,会影响实参指向的对象的内容。

==和equals的区别:

  • ==如果是基本类型比较的是值是否相等,如果是引用类型比较的是指向的内存地址是否相等。
  • equals如果没有重写比较的是地址是否相等,重写了可以比较值是否相等。

    Double类型的比较

    因为二进制的小数无法精确表达10进制小数,所以直接使用double==判断会产生误差,需要使用new BigDecimal(value.toString)转成BigDecimal对象进行比较。
    BigDecimal原理:把十进制小数扩大N倍让它在整数的维度上进行计算,并保留相应的精度信息。scale记录精度信息,intCompact记录被放大的整数信息。

    equals与hashCode

    若是对象,需要同时重写这两者,两者重写与不重写如下所示:

不重写 重写
hashCode 比较内存地址,具体jvm可能不同 自定义逻辑
equals 比较内存地址 自定义逻辑

Java对象的eqauls方法和hashCode方法是这样规定的:

  1. 相等(相同)的对象必须具有相等的哈希码(或者散列码);
  2. 如果两个对象的hashCode相同,它们并不一定相同。

    只重写Equals会怎么样?

    如果是list等集合类,判断是否相等是靠equals,而hash类存储结构(HashSet、HashMap等等)添加元素会有重复性校验,校验的方式就是先取hashCode判断是否相等,然后再取equals方法比较,最终判定该存储结构中是否有重复元素。如果不重写,那么会使值相等的两个元素同时插入进不同的桶值,使之不符合预期。

    ArrayList与LinkedList的区别

  • 数据结构不同ArrayList底层是Object数组,而LinkedList底层维护了Node类型的链表。
  • 效率不同查找元素时,ArrayList获取元素为O(1),LinkedList为O(n);当对数据的中间部分进行增加和删除时,LinkedList效率更高。ArrayList会使用System.arraycopy()进行数据迁移操作。

    异常体系

    image-20200302134849364.png

    反射

    反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

    工作原理

    在类加载的加载阶段,类加载器会根据类的全限定名读取二进制字节流到JVM的内部,并存储在元空间,在将其转换为目标类型对应的java实例,而反射就是在这一阶段读取对应的class文件从而获取相应的信息。

    工作流程

    首先通过.forName()获取目标对象,通过目标对象的getDeclaredField()方法获取字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
    获取类:
  1. .forName(全限定名)
  2. 类名.class
  3. 对象.getClass()

获取内容:

  1. getDeclaredMethods()
  2. getDeclaredFields()
  3. getDeclaredConstructors()

使用:

  1. 方法.invoke(参数)
  2. 构造方法.newInstance()
  3. 类对象.newInstance()

    内部类

    内部类分为成员内部类,静态内部类,局部内部类和匿名内部类。
    成员内部类特点:

  4. 不允许有静态变量或方法;

  5. 依附外部类,只有创建外部类才能创建内部类。

静态内部类特点:

  1. 不依赖外部类;
  2. 不可以使用任何外部类的非static属性或方法。

局部内部类特点:

  1. 类不允许使用访问权限修饰符;
  2. 除了方法,完全隐藏;
  3. 方法形参必须是final。

匿名内部类特点:

  1. 必须继承一个抽象类或者一个接口;
  2. 没有构造方法;
  3. 方法形参必须为final。

所有内部类均可访问可访问的外部类属性和方法,外部类可以通过创建内部类实例来间接访问内部类。

为什么需要final

  • 类的生命周期由于内部类不会随着外部类毁灭而毁灭,因此为了保证数据不会引用到空对象,所以需要拷贝一份参数。
  • 数据一致性为了实现数据一致性,防止原先的局部变量发生改变以至于内部类得不到通知,使程序结果与预期不同,所以需要final修饰访问的参数。

    抽象类和接口的区别

    |
    | 抽象类 | 接口 | | —- | —- | —- | | 是否有非抽象方法 | 可以有非抽象方法 | 全是抽象方法 | | 是否有构造方法 | 有 | 没有 | | 是否有普通成员变量 | 有 | 没有 | | 访问权限 | 没有限制 | 必须是public权限 | | 是否有静态方法和静态代码块 | 有 | 没有(java8有) | | 方法参数类型 | 没有限制 | 必须是static final | | 继承/实现 | 单继承 | 可以多实现 | | 抽象目标 | 对类抽象 | 对行为抽象 | | 设计思想 | 模板式设计 | 辐射式设计 |

泛型

泛型即参数化类型,是编译器的概念。(Array不支持泛型)

使用泛型的好处

  1. 类型安全,防止出现ClassCastException异常;
  2. 消除强制转换。

    类型擦除

    当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这就叫做类型擦除

    限定通配符与非限定通配符

    限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
    另一方面<?>表 示了非限定通配符,因为<?>可以用任意类型来替代。

    List<?>和List的区别List<?> 是一个未知类型的List,而List 其实是任意类型的List。你可以把List, List赋值给List<?>,却不能把List赋值给 List

    注解

    注解通过 @interface 关键字进行定义,简单理解就是贴标签。

    元注解

    元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。其实就是普通注解的描述。
    @Retention
    规定注解的存活时间,具体值如下:
    • RetentionPolicy.SOURCE注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
    • RetentionPolicy.CLASS注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
    • RetentionPolicy.RUNTIME注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

      @Documented

      它的作用是能够将注解中的元素包含到 Javadoc 中去。

      @Target

      @Target 指定了注解运用的地方,即限定使用场景,具体值如下:

    • ElementType.ANNOTATION_TYPE可以给一个注解进行注解。

    • ElementType.CONSTRUCTOR可以给构造方法进行注解。
    • ElementType.FIELD可以给属性进行注解。
    • ElementType.LOCAL_VARIABLE可以给局部变量进行注解。
    • ElementType.METHOD可以给方法进行注解。
    • ElementType.PACKAGE可以给一个包进行注解。
    • ElementType.PARAMETER可以给一个方法内的参数进行注解。
    • ElementType.TYPE可以给一个类型进行注解,比如类、接口、枚举。
      @Inherited
      如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
      @Repeatable
      这个注解可以有多个不同的属性。

      注解的属性

      注解的属性也叫做成员变量,注解只有成员变量,没有方法。如下:
      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface TestAnnotation {
      public int id() default -1;
      public String msg() default “Hi”;
      }
      其中,default是为属性赋予默认值。使用方式如下:
      // 有默认值
      @TestAnnotation()
      public class Test {

      }

    // 没有默认值
    @TestAnnotation(id=-1,msg=’Hi’)

    // 只有一个id属性
    @TestAnnotation(-1)

    // 没有任何属性
    @TestAnnotation

    Java预置的注解

    • @Deprecated告诉开发者,这是一个过时的元素。
    • @Override要重写的方法。
    • @SuppressWarnings忽略被编译器提醒的警告。
    • @SafeVarargs参数安全类型注解。提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。
    • @FunctionalInterface函数式接口注解,所谓的函数式编程是Java8引入的概念,它是仅有一个方法的普通接口。

      如何使用

    1. 使用AOP进行对特定注解的拦截,执行自己的处理逻辑。如限流、打印日志、报告异常等。
    2. 使用反射,对特定注解进行拦截,执行相应的处理操作。

      IO

      数据在两设备间的传输称为流。流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

      分类

      按操作对象来分:

    3. 文件(file):FileInputStream、FileOutputStream、FileReader、FileWriter

    4. 数组([]):
      • 2.1、字节数组(byte[]):ByteArrayInputStream、ByteArrayOutputStream
      • 2.2、字符数组(char[]):CharArrayReader、CharArrayWriter
    5. 管道操作:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
    6. 基本数据类型:DataInputStream、DataOutputStream
    7. 缓冲操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
    8. 打印:PrintStream、PrintWriter
    9. 对象序列化反序列化:ObjectInputStream、ObjectOutputStream
    10. 转换:InputStreamReader、OutputStreWriter

    从运输方式角度来分:

    1. 字节流
    2. 字符流

    字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件。字节流继承inputStream和OutputStream,字符流继承自Reader和Writer。
    image-20200217181944339.png

    字节流和字符流的区别


    字节流 字符流
    是否用到缓冲区(内存) 不用 需要
    操作对象 任何类型,包括二进制 只能处理字符或者字符串
    祖先 InputStream/OutputStream Reader/Writer
    使用场景 大部分情况 频繁对字符操作

    RandomAccessFile

    它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。他是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件

    System.out.println

    println是PrintStream的一个方法。out是一个静态PrintStream类型的成员变量,System是一个java.lang包中的类,用于和底层的操作系统进行交互。

    缓冲区

    1. 缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性。
    2. 对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。

      PrintStream、BufferedWriter、PrintWriter

    • PrintStream输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。用来处理字节流。
    • BufferedWriter将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符串的高效写入。通过write()方法可以将获取到的字符输出,然后通过newLine()进行换行操作。BufferedWriter中的字符流必须通过调用flush方法才能将其刷出去。并且BufferedWriter只能对字符流进行操作。如果要对字节流操作,则使用BufferedInputStream。
    • PrintWriter与PrintStream相似,用于处理字符流。

      节点流与处理流

    • 节点流直接与数据源相连,用于输入或者输出。

    • 处理流在节点流的基础上对之进行加工,进行一些功能的扩展。

    处理流的构造器必须要传入节点流的子类。
    image-20200217183940566.png

    流的关闭操作

    1. 流一旦打开就必须关闭,使用close方法。
    2. 放入finally语句块中(finally 语句一定会执行)。
    3. 调用处理流后就关闭处理流。
    4. 多个流互相调用只关闭最外层的流。

      同步与异步

      1、同步请求所需时间相对来说较长,异步较短;
      2、同步会造成线程阻塞,但是异步执行不会造成自己的线程阻塞;
      3、同步需要等待所有步骤执行完了才能继续往下执行,异步只需要发起调用后就可以继续其他逻辑。

      IO/NIO

      阻塞 IO 模型

      最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。

      非阻塞 IO 模型

      当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。 所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:

      1. while(true){
      2. data = socket.read();
      3. if(data!= error){
      4. 处理数据
      5. break;
      6. }
      7. }

      但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。

      多路复用 IO 模型

      多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。
      另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。
      不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

      信号驱动 IO 模型

      在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。

      异步 IO 模型

      异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。
      也就说在异步 IO 模型中,IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。

      Java8新特性

    5. Lambda表达式——允许把函数作为方法的参数;

    6. 方法引用——直接引用已有的Java类或对象的方法或构造器;
    7. 默认方法——接口里面有一个默认实现方法;
    8. 新工具——新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps;
    9. Stream API——函数式编程;
    10. Date Time API——加强对日期与时间的处理;
    11. Optional类——解决空指针异常;
    12. Nashorn, JavaScript 引擎—— Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。

      Lambda表达式

      例子:new Thread(() -> System.out.println(“thread”));
      ()是接口方法的括号,当有多个抽象方法就不能表示了,所以函数型接口是支持只有一个抽象方法的接口。
      表达式特征:
    • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
    • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
    • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
    • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

    方法引用:

    1. 对象::实例方法,将lambda的参数当做方法的参数使用:Consumer sc = System.out::println;
      //等效
      Consumer sc2 = (x) -> System.out.println(x);
      sc.accept(“hey”);
    2. 类::静态方法,将lambda的参数当做方法的参数使用:Function sf = String::valueOf;
      //等效
      Function sf2 = (x) -> String.valueOf(x);
      String apply1 = sf.apply(123);
    3. 类::实例方法,将lambda的第一个参数当做方法的调用者,其他的参数作为方法的参数。开发中尽量少些此类写法,减少后续维护成本:BiPredicate sbp = String::equals;
      //等效
      BiPredicate sbp2 = (x, y) -> x.equals(y);
      boolean test = sbp.test(“a”, “A”);

    构造引用:

    1. 无参的构造方法是类::实例方法模型:Supplier us = User::new;
      //等效
      Supplier us2 = () -> new User();
      //获取对象
      User user = us.get();
    2. 有参的构造方法://一个参数,参数类型不同则会编译出错
      Function uf = id -> new User(id);
      //或加括号
      Function uf2 = (id) -> new User(id);
      //等效
      Function uf3 = (Integer id) -> new User(id);
      User apply = uf.apply(61888);
      //两个参数
      BiFunction ubf = (id, name) -> new User(id, name);
      User happy = ubf.apply(618, “狂欢happy”);

      接口的默认方法

      实际的例子有List接口的sort方法,它是一个默认方法:
      public interface List extends Collection {

      // …其他成员

      default void sort(Comparator<? super E> c) {


      }
      }
      三条规则:

    3. 类中的方法优先级最高,类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。

    4. 如果第一条无法判断,那么子接口的优先级更高:方法签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
    5. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

      LocalDateTime

      LocalDate
    6. 创建://获取当前年月日
      LocalDate localDate = LocalDate.now();
      //构造指定的年月日
      LocalDate localDate2 = LocalDate.of(2020, 2, 12);

    7. 获取年、月、日、星期:int year = localDate.getYear();
      int year1 = localDate.get(ChronoField.YEAR);
      Month month = localDate.getMonth();
      int month1 = localDate.get(ChronoField.MONTH_OF_YEAR);
      int day = localDate.getDayOfMonth();
      int day1 = localDate.get(ChronoField.DAY_OF_MONTH);
      DayOfWeek dayOfWeek = localDate.getDayOfWeek();
      int dayOfWeek1 = localDate.get(ChronoField.DAY_OF_WEEK);

      LocalTime

      创建和获取与LocalDate相似,不再赘述。

      LocalDateTime

      创建和获取与LocalDate相似,不再赘述。

      格式化
    8. LocalDate转String:LocalDate localDate = LocalDate.now();
      // BASIC_ISO_DATE yyyyMMdd
      String s1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
      // ISO_LOCAL_DATE yyyy-MM-dd
      String s2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
      // 自定义
      DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(“dd/MM/yyyy”);
      String s3 = localDate.format(dateTimeFormatter);

    9. String转LocalDate:String date = “20200212”;
      // 自定义
      DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(“yyyyMMdd”);
      LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.BASIC_ISO_DATE);
      LocalDate localDate2 = LocalDate.parse(date, dateTimeFormatter);

      Optional类

      Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用null 表示一个值不存在,现在Optional 可以更好的表达这个概念。并且可以避免空指针异常。
      常用方法:
    • Optional.of(T t) : 创建一个Optional 实例;
    • Optional.empty() : 创建一个空的Optional 实例;
    • Optional.ofNullable(T t):若t 不为null,创建Optional 实例,否则创建空实例;
    • isPresent() : 判断是否包含值;
    • get():如果调用对象包含值,返回该值,否则抛出异常;
    • orElse(T t) : 如果调用对象包含值,返回该值,否则返回t;
    • orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回s 获取的值;
    • map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty();
    • flatMap(Function mapper):与map 类似,要求返回值必须是Optional。

      Stream API

      Stream是Java8的新特性之一,从名字看出来,它的含义是流,所谓的流就是一个数据渠道,可以用于操作集合所生成的元素序列。 实际上不光是集合,包括数组、文件等,只要是可以转换成流,我们都可以借助流式处理,类似于我们写SQL语句一样对其进行操作。
      特点:
    1. Stream自己不会存储元素。
    2. Stream不会改变源对象。
    3. Stream操作是延时执行的,这意味着它们会等到需要结果时才执行。

    流式处理:
    Stream的流式处理可以分为三个部分:转换成流、中间操作、终端操作,具体例子:
    List userIds = userLists.stream().map(UserDO::getId).collect(Collectors.toList());
    stream语句将userLists集合转换成了一个流,map为中间操作,通过函数式表达式获取user的Id,最后通过终端操作将其转换成一个list集合返回。
    创建流的方式:

    1. .stream()/.parallelStream();
    2. Arrays.stream();
    3. Steam.of(“123”,”456”);

    中间操作与结束操作:
    image-20200212211912365.png
    流水线原理:

    • 操作的记录

    注意,这里的操作是指中间操作,Stream中会使用Stage概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线,其中Stream相关类和接口的继承关系如上图所示。
    Stream流水线组织结构示意图如下:
    image-20200212214032084.png
    通过Collection.stream()方法得到Head,也就是stage0,紧接着调用一系列中间操作,不断产生新的Stream,这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。

    • 操作的叠加

    现在我们知道Stream()是如何记录每一步的操作了,要想让流水线将所有的操作叠加在一起,还需要有一种协议来协调相邻的Stage之间的关系。这个协议就是Sink接口,Sink接口方法如下:
    image-20200212214312111.png
    有了此协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道内部是如何处理的。对于短路操作,也要实现cancellationRequested()。实际上Stream API内部实现的本质,就是如何重载Sink的这四个接口方法。

    • 执行结果的存放

    image-20200212214438029.png

    1. 对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
    2. 对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
    3. 对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。

      其他

    4. HashMap前后变化链表长度大于8时采取红黑树的结构存储。

    5. ConcurrentHashMap从分段锁改为CAS+Synchronized。

      幂等性如何保证

      幂等性是指操作一次与操作多次的返回结果需要保持一致。
      通常来讲,有四种解决方案:

    6. 前端拦截;

    7. 使用数据库实现幂等性;
    8. 使用 JVM 锁实现幂等性;
    9. 使用分布式锁实现幂等性。

      前端拦截

      前端拦截是指通过 Web 站点的页面进行请求拦截,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态,避免用户重复点击。

      使用数据库实现幂等性

    10. 通过悲观锁来实现幂等性;

    11. 通过唯一索引来实现幂等性;
    12. 通过乐观锁来实现幂等性。

      使用JVM锁实现幂等性

      JVM 锁实现是指通过 JVM 提供的内置锁如 Lock 或者是 synchronized 来实现幂等性。使用 JVM 锁来实现幂等性的一般流程为:首先通过 Lock 对代码段进行加锁操作,然后再判断此订单是否已经被处理过,如果未处理则开启事务执行订单处理,处理完成之后提交事务并释放锁。

      分布式锁实现幂等性

      分布式锁实现幂等性的逻辑是,在每次执行方法之前先判断是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可。需要注意的是分布式锁的 key 必须为业务的唯一标识。

    拿记账来说,如果上游对同一笔交易发起了多次记账调用,我们记账业务流程中,凭证插入的时候,会设置唯一索引,幂等字段是client_id与app_name(两个由上游传入),凭证号由发号器统一生成,防止业务多次调用数据库进行多次插入,同时还会锁住账号表的商户号行,本地事务执行完毕后,进行释放。

    不过保证幂等要根据实际的业务情况和操作类型来进行区分。例如,我们在进行查询操作和删除操作时就无须进行幂等性判断。查询操作查一次和查多次的结果都是一致的,因此我们无须进行幂等性判断。删除操作也是一样,删除一次和删除多次都是把相关的数据进行删除(这里的删除指的是条件删除而不是删除所有数据),因此也无须进行幂等性判断。

    main方法为什么必须是public static void?

    在语言规范中,Java虚拟机通过加载指定的类,然后调用该类中的main方法而启动,也就是说,通过调用某个指定类的main方法,传递给他单个的字符数组参数,就可以启动Java虚拟机,而一个main方法想要被执行,需要进行类加载、之后需要进行链接和初始化,之后才是调用main方法。
    首先,必须使用public保证JVM能直接调用,否则无法访问;static能保证Java虚拟机根据类名在运行时数据区的方法区内找到它,同时,由于未被实例化,所以也必须是静态方法,否则无法被调用;Java的退出时,不依赖特殊的exit code,而是在1.所有非守护线程都执行完毕后,调用Shutdown hook退出,2.某个线程调用了Runtime类或System类的exit方法,所以可以是void;另外由于参数可能不是唯一的,所以需要一个字符串数组。

    LongAddr和AtomicLong的区别

    AtomicLong底层原理使用了cas,而且,由于AtomicLong持有的成员变量value是volatile关键字修饰的,线程修改了临界资源后,需要刷新到其他线程,也是要费一番功夫的。而LongAdder也有一个volatile修饰的base值,但是当竞争激烈时,多个线程并不会一直自旋来修改这个值,而是采用了分段的思想。竞争激烈时,各个线程会分散累加到自己所对应的Cell[]数组的某一个数组对象元素中,而不会大家共用一个。
    这样做,可以把不同线程对应到不同的Cell中进行修改,降低了对临界资源的竞争。本质上,是用空间换时间。