学习笔记-Java基础

176 阅读24分钟

Java基础概念

java语言有哪些特点

  • 面向对象: 封装,继承,多态
  • 平台无关: 虚拟机机制,使得Java 语言在不同平台上运行不需要重新编译
  • 安全性: Java提供一些列安全防护措施和访问修饰符,限制直接访问操作系统
  • 可靠性: 异常处理与内存管理
  • 高效性: 执行效率还行
  • 解释和编译并存: Java先将程序编译为字节码,然后交给JVM解释器来运行

JVM、JDK、JRE的关系

JDK是功能齐全的Java SDK,它包括JRE,以及javac(java编译器),还有一些常用的工具:javadoc(文档注释工具),jdp(调试工具),jconcole(可视化监控工具),javap(反编译工具)

JRE是Java运行环境,它包含两个部分:JVM和Java 核心类库

JVM是运行Java字节码的虚拟机,针对不同的操作系统有不同的实现,目的是相同的字节码文件能产生相同的结果。JVM不止一种,只要满足规范,任何人都可以开发自己的JVM

image-20210219163725268

什么是字节码?采用字节码的好处

在java中,字节码(.class文件,十六进制组成)是指能够被JVM理解的代码。

它不面向任何特定的机器,只面向虚拟机,因此将Java编译成字节码文件后,在任何操作系统上都可以运行。这也使得Java具有优异的跨平台移植性以及不错的执行效率。

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言(Java 通过引入JIT,可以理解为编译型语言,但整体上通过JVM解释运行)执行效率低的问题,同时又保留了解释型语言可移植的特点。

JIT和AOT

在JVM中,需要解释器逐行将字节码翻译成机器码执行程序,这样的效率不高。

因此引入了JIT,它属于即时编译,他采用惰性评估,在运行时找到热点代码,并编译成机器码后进行保存,从而提升了java运行效率。

AOT是直接将所有字节码编译成机器码,这种方式效率很高,但是与java语言的动态特性不太兼容,动态特性例如:在运行时生成并加载修改后的字节码

JAVA和C++的区别

  • java不提供指针来直接访问内存
  • C++ 支持多继承,而 Java 不支持多重继承,但允许一个类实现多个接口
  • java有自动内存管理垃圾回收机制,无法手动释放内存
  • java只能重载方法,c++还可以重载操作符

基础语法

基本类型和包装类型的区别

  • 用途:基本数据类型一般用于常量和局部变量,而包装类一般用于方法参数,并且可以使用泛型
  • 存储位置:基本数据类型存于栈中,包装类(非static)存储在堆中
  • 大小:基本数据类型占用空间很小,而对象会相对较大
  • 比较方式:基本数据类型可以直接用==比较,而包装类之间比较用equals
  • 默认值:包装类的默认值为null

包装类型的缓存机制

Byte、Short、Integer、Long缓存了[-128, 127]的值,Character缓存了[0, 127]的值,Boolean缓存了true和false

因此当自动装箱(valueOf方法)时,如果是缓存中的值则直接访问缓存对象,因此出现以下现象

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true

Integer i1 = 255;
Integer i2 = 255;
System.out.println(i1 == i2);// 输出 fasle

Integer i1 = 40;
Integer i2 = new Integer(40);   // 创建了新对象
System.out.println(i1==i2);  // 输出false

静态方法为什么不能调用非静态成员

因为静态方法在类加载时就同时加载了,而非静态成员是在类实例化的时候才加载。相当于静态方法加载的时候,非静态成员还不存在,因此无法调用

重载和重写的区别

重载:指在同一个类中,方法名相同,但是参数列表(个数,类型,顺序)、返回值、修饰符、异常可以不同;重载用于让同一个方法对不同的参数列表实现不同的逻辑

重写:发生在子类中,子类继承父类的方法可以将其重写,重写的方法名必须相同,参数列表必须相同,返回值、修饰符以及异常要小于父类;重写用于让子类改造父类方法的内部逻辑

接口和抽象类的区别

  • 一个类可以实现多个接口,但只能继承一个抽象类
  • 接口中没有构造方法和普通成员变量,抽象类中可以有构造方法和成员变量
  • 接口和抽象类都可以有静态成员变量,但接口默认且只能public final修饰,抽象类不限制
  • 抽象类可以有普通方法;接口只能有抽象方法,在JDK8可以有默认方法,在JDK9后可以有私有方法
  • 抽象类可以有静态方法;接口没有,在JDK8可以有,且只能被接口类所调用

访问修饰符public、private、protected、以及默认的区别

Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
image-20210219173433142

面向对象

面向对象和面向过程的区别

两者的区别在于解决问题的方式不同:

**面向过程:**将解决问题的过程拆分成一个个方法进行解决

  • 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  • 缺点:没有面向对象易维护、易复用、易扩展。

**面向对象:**会抽象出一个对象,通过对象的方法来解决问题;同时具有易维护、易复用、易拓展的优点

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
  • 缺点:性能比面向过程低。

面向对象三大特征

  • 封装:对象可以隐藏自己的属性和方法不被外界访问
  • 继承:通过继承子类可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
  • 多态性:它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

构造方法有哪些特点

  • 方法名与类名一致
  • 没有返回值,不能用void修饰
  • 创建对象时自动调用
  • 不可重写,但可以重载

对象相等判断

== 和equals 的区别

== 常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较;

  • 如果==比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;
  • 如果==是比较的两个对象,那么比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域;

equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象

  • 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
  • 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容

介绍下hashCode()

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置(哈希码对数组的长度取余后会确定一个存储的下标位置)。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。

为什么要有hashCode?

以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,

如果没有相符的hashcode,HashSet会假设对象没有重复出现。

如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

引用拷贝、深拷贝和浅拷贝区别?

**引用拷贝:**直接复制对象的引用地址

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。

深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

shallow&deep-copy

String 相关

String、StringBuffer和StringBuilder的区别

  • 可变与不可变:

    • String类中使用字符数组保存字符串,因为有**final**修饰符,所以string对象是不可变的。

      对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.

    • StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。

  • 是否线程安全:

    • String中对象是不可变的,可以理解为常量,因此线程安全

    • StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的

    • StringBuilder是非线程安全的

  • 性能

    • 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象,因此性能较差

    • StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。

      相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 的性能提升,但却要冒多线程不安全的风险。

String为什么要设计成不可变的?

  • 便于实现字符串池(String pool)

  • 使多线程安全

  • 加快字符串处理速度:由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。

什么是字符串常量池?

**字符串常量池:**jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。

在jdk8中,常量池的位置再元空间(永久代(方法区)被元空间取代)

Java中三个常量池概念:

  • **全局字符常量池**在每个JVM中只有一份,存放的是字符串常量的引用值。
  • **class常量池**是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
  • **运行时常量池**是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

String str="aaa"与 String str=new String("aaa")一样吗

  • 使用String a = “aaa” ;,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。
  • 使用String b = new String("aaa");,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。

String.intern()

将字符串保存到常量池:如果有了就返回引用;没有就创建并返回

字符串+

“+”和“+=”在字符串操作时,java是对这两个操作符重载了,在底层,会创建StringBuilder对象,使用append方法拼接,然后返回toStirng方法产生的对象

但是在循环场景下会大量地创建StringBuilder对象,导致性能也不高,最好还是自己使用StringBuilder(java9后没有这个问题)

反射

什么是反射

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

反射机制的优缺点

优点:能够运行时动态获取类的实例,提高灵活性;可与动态编译结合Class.forName('com.mysql.jdbc.Driver.class'),加载MySQL的驱动类。

缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflectASM工具类,通过字节码生成的方式加快反射速度。

如何获取反射中的Class 对象

  1. Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。

    Class clz = Class.forName("java.lang.String");
    
  2. 类名.class。这种方法只适合在编译前就知道操作的 Class。

    Class clz = String.class;
    
  3. 对象名.getClass()。

    String str = new String("Hello");
    Class clz = str.getClass();
    
  4. 如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。

反射的使用步骤

  1. 获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
  2. 调用 Class 类中的方法,既就是反射的使用阶段。
  3. 使用反射 API 来操作这些信息。

具体可以看下面的例子:

public class Apple {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用
        // 1、获取类的Class对象实例
        Class clz = Class.forName("com.chenshuyi.api.Apple");
		// 2、根据Class对象实例获取Constructor对象
        Constructor appleConstructor = clz.getConstructor();
        // 3、根据Constructor对象的newInstance方法获取反射类对象
        Object appleObj = appleConstructor.newInstance();
        // 4、获取方法的Method对象
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        // 5、利用invoke方法调用方法
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:

Apple Price:5
Apple Price:14

反射机制的原理

深入理解java反射原理 - 阿牛20 - 博客园 (cnblogs.com)

反射的应用场景

  • 动态代理(Spring中xml的配置模式。)
  • 注解

模拟 Spring 加载 XML 配置文件:

public class BeanFactory {
       private Map<String, Object> beanMap = new HashMap<String, Object>();
       /**
       * bean工厂的初始化.
       * @param xml xml配置文件
       */
       public void init(String xml) {
              try {
                     //读取指定的配置文件
                     SAXReader reader = new SAXReader();
                     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                     //从class目录下获取指定的xml文件
                     InputStream ins = classLoader.getResourceAsStream(xml);
                     Document doc = reader.read(ins);
                     Element root = doc.getRootElement();  
                     Element foo;
                    
                     //遍历bean
                     for (Iterator i = root.elementIterator("bean"); i.hasNext();) {  
                            foo = (Element) i.next();
                            //获取bean的属性id和class
                            Attribute id = foo.attribute("id");  
                            Attribute cls = foo.attribute("class");
                           
                            //利用Java反射机制,通过class的名称获取Class对象
                            Class bean = Class.forName(cls.getText());
                           
                            //获取对应class的信息
                            java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
                            //获取其属性描述
                            java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
                            //设置值的方法
                            Method mSet = null;
                            //创建一个对象
                            Object obj = bean.newInstance();
                           
                            //遍历该bean的property属性
                            for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {  
                                   Element foo2 = (Element) ite.next();
                                   //获取该property的name属性
                                   Attribute name = foo2.attribute("name");
                                   String value = null;
                                  
                                   //获取该property的子元素value的值
                                   for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
                                          Element node = (Element) ite1.next();
                                          value = node.getText();
                                          break;
                                   }
                                  
                                   for (int k = 0; k < pd.length; k++) {
                                          if (pd[k].getName().equalsIgnoreCase(name.getText())) {
                                                 mSet = pd[k].getWriteMethod();
                                                 //利用Java的反射极致调用对象的某个set方法,并将值设置进去
                                                 mSet.invoke(obj, value);
                                          }
                                   }
                            }
                           
                            //将对象放入beanMap中,其中key为id值,value为对象
                            beanMap.put(id.getText(), obj);
                     }
              } catch (Exception e) {
                     System.out.println(e.toString());
              }
       }
      
       //other codes
}

注解

什么使注解,有什么作用

注解是一种特殊的注释,用于修饰方法、类、变量,提供信息在运行时使用,具体作用:

  • 生成文档
  • 编译检查
  • 编译时处理,例如动态生成代码
  • 运行时处理,例如反射创建对象

注解的运行方法

注解在解析后才会生效,分为两种:

  • 编译时解析:在编译时进行检查,例如@Override
  • 运行时解析:例如Spring提供的注解,@component,在运行时通过反射处理

泛型

什么是泛型

定义:泛型是jdk5引入的新特性,可以将数据类型作为参数传递

作用:增强代码的可读性和可靠性

使用:泛型类,泛型接口,泛型方法

注意:静态方法无法使用类所接受的泛型

项目中哪里用到了泛型

  • 定义自定义返回结果接口
  • 定义通用方法工具类

如何定义一个泛型方法

传入参数Class<T>~类对象,返回值前加上<T>表明泛型方法,在方法内部使用newInstance方法获取实例对象

泛型的上限和下限

List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List或List。

在使用泛型时可以指定泛型的上限和下限

  • 上限:T extends Number表示数字类型的子类
  • 下限:Info<? super String> temp表示只能接受字符串和Object

为什么说java泛型是伪泛型

在jdk1.5引入了泛型擦除,就是在编译阶段java会将所有泛型转为具体的类型,在运行时就跟没有泛型一样

序列化

什么是序列化,什么是反序列化

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将序列化生成的二进制字节流转换成数据结构或对象的过程

常见应用场景:

  • 网络传输
  • 存储到数据库
  • 存储到文件(IO)
  • 存储到内存

哪些字段不会被序列化

  • **transient修饰的变量:**在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。
  • static修饰的变量: 因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.

常见的序列化协议

  • 基于二进制流:Hession,Kryo,ProtoBuf,ProtoStuff
  • 基于文本:json,xml

为什么不推荐使用JDK自带的序列化技术

  • 不支持跨语言
  • 性能差,生成的序列化文件大
  • 存在安全问题

异常

Error和Exception区别是什么

两者都继承Throwable类

  • Exceptiton:程序本身可以处理的异常,可以被try-catch捕获
  • Error:程序本身无法处理的异常,例如虚拟机错误(Virtual MachineError)、内存溢出(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等等

CheckedException和UnCheckedException有什么区别

CheckedException和UnCheckedException都继承自Exception类

  • CheckedException:受检查的异常,在编译时会对其检查,如果没有做异常处理则无法通过编译。常见的受检查异常有:IO 相关的异常、ClassNotFoundExceptionSQLException...

  • UnCheckedException:不受检查的异常,即使不做处理也能通过编译,一般就是RuntimeException,常见的有:

    • NullPointerException(空指针错误)

    • IllegalArgumentException(参数错误比如方法入参类型错误)

    • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)

    • ArrayIndexOutOfBoundsException(数组越界错误)

    • ClassCastException(类型转换错误)
    • ArithmeticException(算术错误)
    • SecurityException (安全错误比如权限不够)
    • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

Throwable常用方法

  • String getMessage:获取异常发生的简要信息
  • String toString:获取异常的详细信息
  • String getLocalizedMessage:获取本地化信息,如果没重写,跟get Message是一样的
  • void printStackTrace: 打印异常信息

finally中可以写return吗

不行,会导致try或catch中的return语句失效

finally一定会执行吗

以下情况不会执行:

  • 虚拟机中断
  • 线程死亡
  • CPU关闭

throw和throws的区别

Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。

throws 关键字和 throw 关键字在使用上的几点区别如下:

  • throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
  • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

异常使用有哪些需要注意的地方

  • 不要把异常定义为静态变量,会导致异常栈信息错乱
  • 抛出的异常要有意义
  • 尽量使用更为具体的异常
  • 打印日志后就不要抛异常

JVM 是如何处理异常的?

在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。

JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

IO

如何从数据传输方式理解IO流

从数据传输角度,IO流可以分为:

  • 字节流:读取单个字节,常用于处理二进制数据,例如图片、视频等等
  • 字符流:读取单个字符,常用于处理文本数据,例如文本文件(本质也是二进制,但采用某种编码使得人可以阅读)

IO流的四个抽象类基类

  • InputStream:输入字节流
  • OutputStream:输出字节流
  • Reader:输入字符流
  • Writer:输出字符流

IO流为什么要分字节流和字符流

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

  • 虚拟机将字节转换为字符的过程比较耗时
  • 如果不知道编码类型,字节流很容易出现乱码问题

随机访问流

随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile

RandomAccessFile 的构造方法如下,我们可以指定 mode(读写模式)。

// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
    throws FileNotFoundException {
    this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete)  throws FileNotFoundException{
  // 省略大部分代码
}

读写模式主要有下面四种:

  • r : 只读模式。
  • rw: 读写模式
  • rws: 相对于 rwrws 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。
  • rwd : 相对于 rwrwd 同步更新对“文件的内容”的修改到外部存储设备。

文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。

IO中有哪些设计模式

  • 装饰器模式:可以在不改变原有对象的情况下拓展功能;适用于继承关系非常复杂的场景(相当于不用继承就获得核心功能);例如:BufferedInputStream(字节缓冲输入流)创建时可以传入 FileInputStream ,从而增加缓冲功能

    new BufferedInputStream(new FileInputStream(inputStream));
    
  • 适配器模式:让接口不兼容的类协调工作,例如将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。

    Reader reader = new InputStreamReader(inputStream);
    

    img

  • 工厂模式:用于创建对象,NIO 中大量用到了工厂模式,比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)

  • 观察者模式:NIO中的文件目录监听用到了观察者模式,

什么是同步和阻塞?

  • 阻塞和非阻塞:程序级别的,描述程序在访问IO资源时,如果IO资源还没有准备好,前者等待,后者继续执行,并且使用线程不断询问直到准备好
  • 同步和非同步:操作系统级别的,描述操作系统在收到程序访问IO请求时,如果IO资源还没准备好,前者不响应直到资源准备后,后者会返回一个标记(后续进行通知的地方),等资源准备好后再用事件机制返回给程序

什么是多路复用IO

系统调用由多个任务组成,将这些任务拆分执行

什么是信号驱动IO

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

BIO、NIO 和 AIO 的区别?

BIO:同步阻塞,线程会一直等待阻塞直到IO操作结束

NIO:同步非阻塞,线程会先询问是否准备就绪,如果没有准备好先去做别的事,过会儿再来问

AIO:异步非阻塞,线程发起IO请求后就返回,让别的线程去做IO操作,操作完了再通知

什么是零拷贝,有什么用

传统IO读写数据需要内核空间和用户空间来回复制,在高并发场景下,性能开销比较大

零拷贝的实现

mmap + write:

  • 应用程序调用mmap()后,DMA会将磁盘的数据拷贝到内核缓冲区,接着做映射,使应用程序和内核共享缓冲区
  • 应用程序调用write()后,CPU会直接将内核缓冲区的数据拷贝到socket缓冲区,从而在内核中实现了直接拷贝
  • 最后由socket将数据DMA拷贝到网卡中

这样操作减少了内核态到用户态的拷贝次数(3次),但仍然由CPU搬运数据,过程包含两次系统调用,四次上下文切换

sendfile

  • 网卡不支持 SG-DMA:过程与mmap + write类似,同样由CPU进行拷贝,不过只进行了一次系统调用,两次上下文切换
  • 网卡支持SG-DMA:DMA会将磁盘的数据拷贝到内核缓冲区,网卡使用SG-DMA方式直接从内核缓冲区拷贝数据;这种方式只有两次拷贝,没有在内存空间中拷贝数据,实现了真正的零拷贝