面试整理——Java基础

Java基础

一. 面向对象

问:面向对象的三大特性?分别是怎么使用的,给个例子?

  • 封装:比如类把属性和行为封装起来,只提供公共方法被外界访问,限制直接访问对象内部数据。
  • 继承:允许一个类从另一个类获得属性和方法,从而实现代码的复用。比如父子类间的继承关系,子类继承父类的属性方法。
  • 多态:允许一个对象在不同的上下文中表现为不同的形态。包括两种形式:编译时多态or静态多态(方法重载)运行时多态or动态多态(方法重写)。比如方法重载在同一个类中存在多个同名方法,参数不同;方法重写在子类通过继承并重写父类方法,同样的方法会有不同的实现。

问:多态的原理是什么?

  • 多态是面向对象编程中的核心概念,它允许一个对象在不同的上下文中表现出不同的行为。多态的实现主要依赖于 继承方法重写
  • 多态的底层实现是动态绑定(Dynamic Binding)后期绑定(Late Binding),以及通过父类引用指向子类对象来实现。
    • 通过父类引用指向子类对象:即我们可以通过声明父类的引用变量存储子类对象的引用,即引用实际上指向子类对象,如 Animal animal = new Dog();
    • 动态绑定OR后期绑定:在运行时根据对象的实际类型来绑定的,而不是在编译时就确定好。在运行时才把方法调用与方法实现关联起来,通过方法表来实现,表中记录了类定义方法的指针,指向具体的方法代码,当类重写了父类方法,对应的表项就会指向新的代码实现,所以无论引用变量是父类还是子类,最终运行时都能执行正确的代码。
  • 面向对象-多态

问:讲一下重载和重写?

  • 重载(Overloading) :指在同一个类中定义多个方法,这些方法的名字相同,但参数列表不同(参数类型、数量或顺序不同)。重载是一种编译时多态性(也叫静态多态性),在编译时决定调用哪一个方法。
  • 重写(Overriding):指在子类中重新定义父类中已有的方法。重写是继承机制的一部分,用于运行时多态性(也叫动态多态性),即在运行时根据对象的实际类型调用相应的方法。子类通过重写父类的方法,可以提供自己的实现。
特性 重载(Overloading) 重写(Overriding)
方法名称 相同 相同
参数列表 必须不同 必须相同
返回类型 可以不同 必须相同
发生时机 编译时(静态多态性) 运行时(动态多态性)
所属关系 同一个类中 子类与父类之间
访问修饰符 无要求 访问权限不能比父类方法更严格
@Override 不需要 必须用 @Override 标注,表示方法被重写

二. 数据类型

问:Java的基本类型有哪几个?各自占了多少位?一个char能放一个汉字吗?

  • 8个,byte,short,int,long,float,double,boolean,char
  • 1个字节Byte是8位bit。
    • 整型:byte是8位,short是16位,int是32位,long是64位
    • 浮点型:float是32位(单精度),double是64位(双精度)
    • 字符型:char则是16位,存储一个Unicode字符
    • 布尔型:boolean是1位
  • 可以,char是16位Unicode字符,一个汉字占两个字节,所以够放。

问:基本类型和包装类型的区别?涉及自动装箱和拆箱,怎么做的,原理?

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

  • 基本类型是原始数据类型,而包装类型是基本类型的对象封装。
  • 基本类型有固定的存储空间,包装类则都是对象。
  • 包装类作为对象存放在堆中,而基本类型存放在栈里。随着方法的结束,栈内存会自动释放,因此不需要 GC 管理。如果频繁创建包装类型对象,GC 会更频繁地进行对象回收,影响性能。
  • 缺省值不同,基本类型根据类型有不同的缺省值,而包装类型则为null。

自动装箱(Auto-boxing):基本类型自动转换为包装类型。

拆箱(Unboxing):包装类型自动转换为基本类型。

  • Java 编译器在处理装箱和拆箱时,实际上是通过在编译阶段插入必要的转换代码来实现的。如 整型通过 Integer.valueOf() 以及 Integer.intValue() 分别完成装箱和拆箱。
  • Java 为包装类型提供了一定的缓存优化 IntegerCache(如 Integer 的值在 -128127 范围内时会复用对象)。然而,超过这个范围时,Java 需要为每个新的值分配新的对象,造成额外的内存分配和管理开销。
  • 自动装箱和拆箱过程增加了额外的性能开销,尤其是在频繁进行算术运算或集合操作时。如果频繁装箱、拆箱,会导致更多的内存分配、垃圾回收,影响性能。

问:传值和传引用的区别?为什么说Java只有值传递?

  • 首先什么是值传递和引用传递?
    • 首先这两个专有名词有其特指定义,属于函数调用时参数的求值策略,是对调用函数时,求值和传值方式的描述,而不是对传递内容类型的描述。
    • 应该是根据是否会创建副本,以及是否会影响到原始对象来判断。
      • 传值(Pass-by-Value):将变量的值复制一份传递给方法。方法内对该参数的修改不会影响到原始变量,因为方法操作的是值的副本。
      • 传引用(Pass-by-Reference):传递的是变量的内存地址,方法操作的是真正的变量本身。方法内对该参数的修改会影响到原始变量,因为方法操作的是实际的对象或数据,而不是副本。
    • 值传递和引用传递是区分两种内存分配方式值类型在调用栈上分配,引用类型在堆上分配。值传递会创建副本,无法在函数内改变原值。引用传递则不会创建副本,可以在函数内改变原值。
    • 这些区别和参数类型是值类型或是引用类型无关,如果是值传递,不管是何种类型都会在调用栈上创建一个副本,区别在值类型的副本是原值的复制,引用类型的实例在堆上,在栈上只有它的引用,其副本是引用的复制。
  • 为什么说Java只有值传递?
    • 根据值传递和引用传递的定义,Java中应该只有值传递。因为不管形参是哪种数据类型,最终传递给形参的都是实参的拷贝,函数对形参的赋值操作也不能被调用者感知。这种设计其实对应着求值策略中的传共享对象调用。
    • 即使是对象类型的参数传递,传递的依然是对象引用的副本。
  • 什么是求值策略中的传共享对象调用?
    • 传共享对象调用(Call by Sharing),也称为共享对象传递,是一种参数传递策略,主要应用在某些编程语言(如 Python、Java、Ruby)中。
    • 当方法或函数接收一个对象作为参数时,传递的是对象引用的副本,而不是对象的值或对象的引用本身。

问:货币用哪种数据类型?

  • BigDecimal,因为BigDecimal可以表示精确的浮点数。

  • floatdouble 是浮点数类型,使用二进制存储小数。这些类型在进行加减乘除等运算时容易出现精度问题,尤其是在涉及到货币计算时(例如财务计算需要非常精确的结果),这种精度损失是不允许的。

    如:

    1
    2
    3
    double value1 = 2.15;
    double value2 = 1.10;
    System.out.println(value1 - value2); // 输出 1.0499999999999998
  • BigDecimal 是基于十进制实现的,还提供了多种舍入模式,高精度的加减乘除运算。

    1
    2
    3
    BigDecimal value1 = new BigDecimal("2.15");
    BigDecimal value2 = new BigDecimal("1.10");
    System.out.println(value1.subtract(value2)); // 输出 1.05,精度正确

问:i++++i 的区别?

  • 二者区别在于进行自增操作的时机
  • i++ 是先赋值再运算,++i 是先运算再赋值。
    • i++后置自增,Post-increment)表示在使用变量 i 的当前值之后,再对其进行自增操作。
    • ++i前置自增,Pre-increment),表示在使用变量 i 之前,先对其进行自增操作。
1
2
3
int i = 0, j = 0, k = 0;
j = i++; // j值为0,i为1
k = ++j; // k值为1,j为1

三. 关键字

问:讲讲Java中用来描述作用域的修饰符?

作用域 当前类 同一package 子类 其他package非子类
public
protected ×
default × ×
private × × ×

使用场景:

  1. public:希望让代码中的某个部分可以被所有外部访问时使用。
  2. protected:在继承体系中,希望允许子类访问父类的某些属性或方法时,可以使用 protected。这样保证了继承关系中的灵活性。
  3. default:希望某个成员仅限于同一包中使用时,可以不使用任何访问修饰符,让其默认为包级访问。
  4. private:希望封装类的实现细节,避免外部代码直接访问类的某些字段或方法时使用。

问:assert?

  • assert 是 Java 中的断言机制,用于在代码中设置检查点,验证某个条件是否成立。如果条件不成立,程序将抛出一个 AssertionError。断言通常用于在开发和测试阶段捕捉程序中的逻辑错误。

  • 基本格式:

    1
    2
    assert condition;
    assert condition : expression;

    如:

    1
    2
    3
    4
    5
    6
    7
    public class Test {
    public static void main(String[] args) {
    int age = 15;
    assert age >= 18 : "Age must be 18 or older";
    System.out.println("Age is valid");
    }
    }
  • 在 Java 中,断言默认是禁用的,只有显式启用后才会生效。断言通常在开发阶段使用,而生产环境中一般不启用。在运行程序时,使用 -ea-enableassertions)参数启用断言:java -ea Test 。通过 -da-disableassertions)参数禁用断言(默认状态):java -da Test

问:Java有哪几种移位运算符?

  • << :左移运算符。将 x 的所有位向左移动 n 位,右边补 0。相当于将 x 乘以 2^n

    1
    2
    3
    int x = 3;   // 3 的二进制是 0000 0011
    int result = x << 2; // 将 3 向左移动 2 位,结果是 0000 1100,也就是 12
    System.out.println(result); // 输出 12
  • >> :有符号右移运算符。将 x 的所有位向右移动 n 位,左边用符号位(即原最高位)填充。正数补 0,负数补 1。相当于将 x 除以 2^n(向下取整)。

    1
    2
    3
    4
    5
    6
    7
    int x = 12;  // 12 的二进制是 0000 1100
    int result = x >> 2; // 将 12 向右移动 2 位,结果是 0000 0011,也就是 3
    System.out.println(result); // 输出 3

    int x = -12; // -12 的二进制是 1111 0100(补码形式)
    int result = x >> 2; // 向右移动 2 位,结果是 1111 1101,也就是 -3
    System.out.println(result); // 输出 -3
  • >>> :无符号右移运算符。将 x 的所有位向右移动 n 位,左边总是补 0,不考虑符号位。无符号右移运算只在处理无符号数时有用。

    1
    2
    3
    nt x = -12;  // -12 的二进制是 1111 0100(补码形式)
    int result = x >>> 2; // 无符号右移 2 位,结果是 0011 1101,也就是 1073741821
    System.out.println(result); // 输出 1073741821

问:Switch 是否能作用在 Byte 上,是否能作用在 Long 上,是否能作用在 String 上 ?

switch(expr1) 中,expr1 是一个整数表达式,且只支持较小的整数类型。传递给 switch 和 case 语句的参数应该是 intshortchar 或者 byte 。JDK 1.5引入enum后也可以支持。Java 7开始支持String。综上所述switch支持:byteshortcharint 以及包装类 ByteShortCharacterInteger,还有 enum 枚举类型。long则不支持。

1
2
3
4
5
6
7
8
9
10
11
byte b = 1;
switch (b) {
case 1:
System.out.println("It's 1");
break;
case 2:
System.out.println("It's 2");
break;
default:
System.out.println("Default case");
}

对于特定需要对特殊类型进行判断的场景,可以使用if-else或map来代替switch。

问:switch、if-else、map的对比?

  1. switch:
    • 最简洁、代码可读性高,对于常量值(如 intcharenum 等),编译器可以生成高效的查找表或跳转表,从而提高执行效率。适用于固定且有限的离散值判断。
    • 不支持 long 类型以及复杂的条件表达式。不能用于范围条件(如 x > 10 && x < 20)。
  2. if-else:
    • 灵活性高,可以处理任意布尔表达式和复杂的条件判断,包括范围判断、逻辑运算等。可以支持所有数据类型。适用于复杂条件判断。
    • 条件过多时,代码可读性差。
  3. map:
    • 查找效率高。当编译时无法确定条件值时,可以动态进行条件判断。
    • 需要额外内存,代码可读性差。

问:谈谈 final, finally, finalize ?

  • final:修饰符,用于定义不可变的变量、不可继承的类以及不可覆盖的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 值不能被修改,且必须在声明时初始化,或者在构造函数中初始化。
    final int MAX_VALUE = 100;

    // 不能在子类中被重写(Override)
    public final void display() {
    System.out.println("Display method in Parent");
    }

    // 不能被继承(Subclass)
    public final class FinalClass {
    // class content
    }
  • finally:是一个块,用于异常处理中的 try-catch 结构中。finally 块中的代码无论是否发生异常都会执行。一般用来执行清除工作或缺省处理。

    1
    2
    3
    4
    5
    6
    7
    8
    try {
    // code that may throw an exception
    } catch (Exception e) {
    // handle the exception
    } finally {
    // code that will always execute, regardless of whether an exception occurred
    // e.g., closing files, releasing resources
    }
  • finalize:是 Object 类中的一个方法,目的是在垃圾回收器决定销毁对象之前,进行资源的清理工作。垃圾收集器会确认对象是否已没有引用。

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    protected void finalize() throws Throwable {
    try {
    // cleanup code
    } finally {
    super.finalize();
    }
    }

    finalize 方法的调用时机不可预测,可能导致性能问题。建议使用 try-with-resources 语句和显式的资源管理来替代 finalize。在 Java 9 及更高版本中,finalize 方法已被标记为过时,建议使用 java.lang.ref.Cleanerjava.lang.ref.PhantomReference 作为替代方案。

问:谈谈 static ?以及为什么Java不允许静态方法访问非静态变量?

  • static 是一个修饰符,用于声明类级别的成员(变量、方法和块)。

    • 静态变量:所有实例共享的,所有对象都访问同一份数据。只在类加载时初始化,并且只初始化一次。

      1
      2
      3
      4
      5
      6
      7
      8
      class Counter {
      static int count = 0; // 类变量

      void increment() {
      count++;
      }
      }
      // 每个 Counter 类的实例都会共享同一个 count 变量。
    • 静态方法:可以在没有创建类实例的情况下被调用。可以通过类名直接调用 static 方法。static 方法只能访问 static 变量和调用其他 static 方法。不能直接访问实例变量和实例方法(非静态)。

      1
      2
      3
      4
      5
      6
      7
      class MathUtils {
      static int add(int a, int b) {
      return a + b;
      }

      int sum = MathUtils.add(5, 10); // 通过类名调用 static 方法
      }
    • 静态块:用于初始化 static 变量。它在类加载时执行,并且在类的所有 static 变量初始化之前执行一次。在类加载时,static 块的执行顺序早于任何静态变量的初始化。

      1
      2
      3
      4
      5
      6
      7
      class Example {
      static {
      System.out.println("Static block executed");
      }

      static int value = 10;
      }
    • 静态内部类:是一个静态嵌套类,它与外部类的实例无关。只能访问外部类的 static 成员,不能直接访问外部类的实例成员。

      1
      2
      3
      4
      5
      6
      7
      class Outer {
      static class Inner {
      void display() {
      System.out.println("Inside static nested class");
      }
      }
      }

为什么Java不允许静态方法访问非静态变量?

  • 非静态变量需要关联一个实例化对象。静态方法在没有对象实例的情况下也可以被调用。
  • 静态方法的生命周期和作用范围是整个类,而非静态变量的生命周期和作用范围是对象实例。

四. 字符串和对象

问:String是不是java的基本类型?

答:不是,String是引用类型,虽然有常量的一些特性。是 Java 标准库中的一个类,定义在 java.lang 包中。

问:String为什么要是final类型的?

  1. 保证字符串的不可变性,从而保证使用字符串时的安全高效
    • 字符串常量池的设计采用享元模式,需要字符串的不可变性。假如字符串可变,共享元素在一处被改变,所有使用者都会感知到变化。
  2. 线程安全:如果字符串可变性会使软件安全性降低,不可变就意味着不会被篡改,同样也意味着线程安全。
  3. 哈希码缓存String 对象在作为键使用时(如在 HashMap 中),其哈希码计算是昂贵的。如果 String 是可变的,每次访问 hashCode 时都可能产生不同的值,因此不能依赖哈希码来维护一致性。
  4. 性能优化:将 String 类声明为 final 允许 JVM 优化 String 对象的存储和重用。常量池技术可以重用相同的 String 实例,减少内存占用和创建开销,提高性能。

问:String,StringBuffer,StringBuilder的区别?

  • String是字符串常量,是不可变的,一旦创建了一个 String 对象,它的内容不能被改变。对 String 对象的任何修改都会创建一个新的 String 对象。
    • 适用于对字符串进行少量操作或不需要频繁修改的场景,例如常量和配置。
  • StringBuffer 是可变的,允许对字符串进行修改而不会创建新的对象。其所有方法都被 synchronized 修饰,以确保在多线程环境中的安全性。
    • 适用于多线程环境中需要频繁修改字符串的场景。由于同步开销,在单线程环境下性能较差。
  • StringBuilder也是可变的,但不进行同步。字符串的拼接操作会隐性的转换为StringBuilder。
    • 适用于单线程环境中需要频繁修改字符串的场景。

问:享元模式?

  • 享元模式(Flyweight Pattern)旨在通过共享对象来减少内存消耗和提高性能。核心思想是将对象的共享和不可变部分与每个对象独有的部分分开,以减少系统的内存占用。

  • 概念:

    • 享元(Flyweight):是可以被共享的对象,主要用来复用对象实例。享元对象包含了对象的共有部分(内蕴状态),这部分是可以被多个对象共享的。
    • 内蕴状态(Intrinsic State):享元对象中可以被共享的部分。内蕴状态是不变的,对象的内部状态,不会随着对象的使用而变化。
    • 外蕴状态(Extrinsic State):享元对象中不能共享的部分,需要在对象外部维护的状态。这部分状态是变化的,可以根据上下文传递给享元对象。
    • 享元工厂(Flyweight Factory):负责创建和管理享元对象,并确保对象的共享。在工厂中可以查找和重用已经创建的享元对象。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // 1. 定义享元接口
    interface Tree {
    void display(String location);
    }

    // 2. 实现具体享元类
    class TreeType implements Tree {
    private String type; // 内蕴状态

    public TreeType(String type) {
    this.type = type;
    }

    @Override
    public void display(String location) {
    System.out.println("Tree Type: " + type + " at location: " + location);
    }
    }

    // 3. 创建享元工厂

    class TreeFactory {
    private static final Map<String, Tree> treeMap = new HashMap<>();

    public static Tree getTree(String type) {
    Tree tree = treeMap.get(type);
    if (tree == null) {
    tree = new TreeType(type);
    treeMap.put(type, tree);
    }
    return tree;
    }
    }

    // 4. 使用享元模式
    public class FlyweightPatternExample {
    public static void main(String[] args) {
    Tree tree1 = TreeFactory.getTree("Pine");
    tree1.display("Forest");

    Tree tree2 = TreeFactory.getTree("Oak");
    tree2.display("Park");

    Tree tree3 = TreeFactory.getTree("Pine");
    tree3.display("Garden");

    // Output:
    // Tree Type: Pine at location: Forest
    // Tree Type: Oak at location: Park
    // Tree Type: Pine at location: Garden
    }
    }

问:字符串编码的区别?

  • 字符串编码是将字符集中的字符转换成计算机可识别的字节序列的过程。
  • 字符串编码用固定的字节来表示一系列字符,常见的如ASCII,Unicode 和 UTF-8等。最早由美国制定的ASCII只包含常见的英文字母和数字等,后来各个国家制定了包含自家语言的编码格式如包含中文的GB2312等。多种标准的诞生也带来了各种冲突和乱码的现象,所以便催生出Unicode标准统一所有语言。
  • ASCII编码是1个字节,Unicode编码通常是2个字节。
  • 因此如果是纯英文文本使用Unicode就要多出一倍的存储空间,为了减少这种浪费,又出现了可变长编码:UTF-8。将一个Unicode字符根据不同的数字大小编码成1-6个字节,英文字母是1个字节,汉字则是3个字节。UTF-8这种特性同时也使它兼容了ASCII编码,使得一些上了历史的软件也能在新的编码标准下工作。目前计算机内存中统一使用Unicode编码,当需要保存到硬盘或进行传输时就转换为UTF-8编码。

问:char 型变量中能不能存贮一个中文汉字?

  • 可以,每个 char 类型变量占用 16 位(2 个字节),足以存放绝大多数正在使用的汉字。

问:简单描述String的原生方法intern?

  • String的 intern() 方法是一个 native原生方法。将字符串对象添加到常量池中的方法,使得相同内容的字符串对象在内存中只存在一份。如果常量池中存在当前字符串,就会直接返回此字符串,若常量池没有,则先将字符串放入常量池内再返回。
  • 字符串常量池(string pool)是一个有固定大小的HashTable,所以当存入的字符串变多时就会导致哈希冲突,使链表过长,导致 String.intern() 性能大幅下降。可以通过参数 -XX:StringTableSize 指定常量池大小。
  • 字符串常量池在Java 7版本前位于非堆区Perm Gen(永久代,HotSpot独有),后迁移到堆中,而Perm Gen在Java 8时被移除,其中的方法区移到了Metaspace(元空间),Metaspace不位于虚拟机内,而是使用本地内存,所以理论上最大可用空间是系统整个内存空间,将元数据剥离出Perm Gen提高GC效率,字符串与类元数据分开提升了独立性。
  • 编译阶段字面量存入Class文件中的常量池表,当类在运行阶段被加载,常量池表会被加载到方法区,并将对应数据存放到运行时常量池。

问:创建字符串时的内部流程?两种字符串实例化方式在内存分配上有区别吗?

  1. 字符串可以通过两种方式创建:直接使用字面量通过 new 关键字
  2. 内部流程:
    • 字面量:
      1. 编译时:编译器将字符串字面量 "hello" 存储在 .class 文件的常量池表中。
      2. 类加载时:类加载器将常量池表中的字符串字面量加载到 运行时常量池
      3. 运行时:当程序执行到 String str1 = "hello"; 语句时:
        • JVM 检查字符串常量池中是否已经存在 “hello” 字符串。
          • 如果存在,则直接返回该字符串对象的引用。
          • 如果不存在,则在堆内存中的 字符串常量池 中创建一个 "hello" 字符串对象,并返回它的引用。
    • 关键字:
      1. JVM 首先查看字面量 "hello" 是否存在于 字符串常量池 中。
        • 如果不存在,首先在 字符串常量池 中创建一个 "hello" 字符串对象。
        • 如果存在,则使用已有的字符串对象。
      2. JVM 接着在 堆内存 中创建一个新的字符串对象,该对象是对字符串 "hello"副本,并将 str2 引用指向堆中的新对象。
  3. 内存分配的区别:字符串常量赋值方式在类加载阶段就实现了实例化,并将字符串驻留在字符串常量池,而 new 实例化对象需要等到执行阶段,并不会和字符串常量池有关联。
    • 字面量:在堆内存中的 字符串常量池 中创建一个 "hello" 字符串对象。
    • 关键字:即使字符串常量池中已经存在相同的字符串对象,JVM 仍然会在堆中重新创建一个新的字符串对象。

问:谈下equals和==的区别?equals和hashcode的用法及区别?为什么重写equals方法时还要重写hashcode方法?

  • 谈下equals和==的区别?
    • == 是运算符基础数据类型直接比较值,引用类型则比较两个对象的引用是否相等,是否指向同一个内存地址。
    • equals比较两个对象的内容是否相等。默认实现来自 Object 类,通常比较对象的引用是否相等,但可以被子类重写以实现内容比较。
  • equals和hashcode的用法及区别?
    • hashcode方法用于创建散列表时获取对象的哈希码值,equals则用于对象的等值判断。当重写 equals 方法时,也应该重写 hashCode 方法,以确保对象的哈希码一致。
    • equals相等则hashcode必定相等,hashcode相等equals未必相等。前者是散列表取值时需要先判断hashcode(hashcode比较效率较高),所以只有hashcode相等才能取到正确的值。哈希值相等的两个对象未必是相等的(哈希冲突),因为哈希码并不是百分百可靠。
  • 为什么重写equals方法时还要重写hashcode方法?
    • 重写equals后很可能导致equals相等时hashcode却不相同,会在许多散列表的使用场景下出错(Set或HashMap取值),哈希值不同导致对象存储在不同的桶中,所以重写equals时也要重写hashcode。

equals和hashCode异同

问:深拷贝与浅拷贝?

  • 浅拷贝是指创建一个新对象,然后将原对象的字段值复制到新对象中。对于基本数据类型字段,浅拷贝会直接复制值;对于引用类型字段,浅拷贝仅复制引用,指向同一个对象。因此,原对象和新对象的引用类型字段将指向同一个内存地址,导致它们共享同一份数据。浅拷贝通常通过 Object.clone() 方法实现。为了支持浅拷贝,需要实现 Cloneable 接口,并重写 clone 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class Person implements Cloneable {
    String name;
    Address address; // Address 是一个引用类型

    public Person(String name, Address address) {
    this.name = name;
    this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone(); // 浅拷贝
    }
    }

    class Address {
    String city;

    public Address(String city) {
    this.city = city;
    }
    }

    public class ShallowCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
    Address addr = new Address("New York");
    Person person1 = new Person("John", addr);
    Person person2 = (Person) person1.clone(); // 浅拷贝

    System.out.println(person1.address == person2.address); // 输出 true,说明地址是共享的
    }
    }
  • 深拷贝是指创建一个新对象,并且递归地复制原对象所引用的所有对象。这样,新对象及其内部对象都与原对象完全独立,不会共享任何数据。深拷贝通常需要手动实现,因为 Java 中没有提供内置的方法来自动进行深拷贝。可以通过序列化+反序列化、复制构造函数、手动递归复制、第三方类库(Apache Commons Lang、Jackson、Gson)等方法实现深拷贝。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class Person implements Cloneable {
    String name;
    Address address;

    public Person(String name, Address address) {
    this.name = name;
    this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    Person cloned = (Person) super.clone(); // 浅拷贝
    cloned.address = new Address(this.address.city); // 手动深拷贝
    return cloned;
    }
    }

    class Address {
    String city;

    public Address(String city) {
    this.city = city;
    }
    }

    public class DeepCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
    Address addr = new Address("New York");
    Person person1 = new Person("John", addr);
    Person person2 = (Person) person1.clone(); // 深拷贝

    System.out.println(person1.address == person2.address); // 输出 false,说明地址是独立的
    }
    }

  • 深拷贝和浅拷贝主要区别在是否支持引用类型的拷贝,浅拷贝对于可变对象无法保证数据安全,深拷贝会把对象里面嵌套对象这种结构完整的拷贝下来。

Java对象克隆

五. 类与接口

问:什么是泛型?类型擦除?

  • 泛型,即参数化类型,把类型当作参数一样传递,增强代码的 类型安全性可重用性可读性与维护性

  • 使用:

    1. 泛型类:允许在声明时定义一个或多个类型参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 定义泛型类,T 是类型参数
      public class Box<T> {
      private T value;

      public void setValue(T value) {
      this.value = value;
      }

      public T getValue() {
      return value;
      }
      }

      // 使用
      Box<String> stringBox = new Box<>();
      stringBox.setValue("Hello");
      System.out.println(stringBox.getValue()); // 输出 "Hello"

      Box<Integer> intBox = new Box<>();
      intBox.setValue(100);
      System.out.println(intBox.getValue()); // 输出 100
    2. 泛型方法:在方法中定义泛型类型,可以用于任何类或接口。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class GenericMethod {
      // 定义一个泛型方法,T 是类型参数
      public <T> void printArray(T[] array) {
      for (T element : array) {
      System.out.println(element);
      }
      }
      }

      // 使用
      GenericMethod gm = new GenericMethod();
      Integer[] intArray = {1, 2, 3};
      String[] strArray = {"A", "B", "C"};
      gm.<Integer>printArray(intArray); // 输出 1 2 3
      gm.<String>printArray(strArray); // 输出 A B C
    3. 泛型接口:类似于泛型类,接口中也可以定义类型参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      public interface GenericInterface<T> {
      T getValue();
      void setValue(T value);
      }

      public class GenericClass<T> implements GenericInterface<T> {
      private T value;

      @Override
      public T getValue() {
      return value;
      }

      @Override
      public void setValue(T value) {
      this.value = value;
      }
      }

      // 使用
      GenericClass<String> genericString = new GenericClass<>();
      genericString.setValue("Hello");
      System.out.println(genericString.getValue());
  • Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理时被擦除,将泛型类型转换为它们的 原始类型(通常是 Object 或限定的上界类型),这个过程即类型擦除

  • 过程:编译期间,Java 会执行以下步骤

    1. 泛型类型替换:编译器会将泛型类型替换为其 限定的类型Object
      • 如果泛型声明了上限(如 <T extends Number>),编译器会将泛型替换为上限类型。用其最左边界(最顶级的父类型)类型替换。
      • 如果没有指定上限,泛型将被替换为 Object
    2. 插入类型检查和类型转换:在插入泛型类型的地方,编译器会自动插入类型检查和类型转换代码,以确保类型安全性。
    3. 生成字节码:擦除后的代码与非泛型代码基本一致,泛型信息在运行时不可见。
  • 泛型方法进行类型擦除后是否会与多态矛盾?虚拟机生成桥方法来保持多态。在子类中生成一个桥方法,也就是对父类方法的重写。

参考:泛型和类型擦除

问:Comparable 和 Comparator 接口是干什么的,其区别?

  • Comparable 是排序接口,表示实现类支持排序,就可以通过工具类Collections.sort或Arrays.sort等进行排序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override
    public int compareTo(Person other) {
    return this.age - other.age; // 按照年龄升序排序
    }

    @Override
    public String toString() {
    return name + ": " + age;
    }
    }

    List<Person> people = new ArrayList<>();
    people.add(new Person("Alice", 30));
    people.add(new Person("Bob", 25));
    people.add(new Person("Charlie", 35));

    Collections.sort(people); // 自动按年龄排序
    System.out.println(people);
  • Comparator 则是比较器接口,实现类本身不支持排序,而是支持对另一个类进行排序的比较器。是一个外部比较器,它用于定义任意两个对象之间的比较方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
    return p1.getAge() - p2.getAge();
    }
    }

    public class NameComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
    return p1.getName().compareTo(p2.getName());
    }
    }

    List<Person> people = new ArrayList<>();
    people.add(new Person("Alice", 30));
    people.add(new Person("Bob", 25));
    people.add(new Person("Charlie", 35));

    // 使用自定义的比较器排序
    Collections.sort(people, new NameComparator()); // 按名字排序
    System.out.println(people);
  • Comparable实现类具有排序属性,相当于内部比较器。Comparator实现类就是一个比较类,相当于外部比较器。**Comparable** 适用于对象有固定的、自然的排序需求,例如数字、日期、姓名按字母顺序排序等。**Comparator** 适用于当你需要对同一类对象使用多种排序逻辑(例如按年龄、按名字等)或对外部无法修改的类进行排序。

  • 在 Java 8 中,Comparator 接口提供了许多实用的默认方法,如 comparing()thenComparing()reversed() 等,可以更加简洁地创建比较器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    List<Person> people = new ArrayList<>();
    people.add(new Person("Alice", 30));
    people.add(new Person("Bob", 25));
    people.add(new Person("Charlie", 35));

    // 按年龄排序
    people.sort(Comparator.comparingInt(Person::getAge));

    // 按名字排序,先按字母升序,再按年龄升序
    people.sort(Comparator.comparing(Person::getName)
    .thenComparing(Person::getAge));

问:接口和抽象类,你是怎么理解的?

  • 从设计来讲接口是对动作的抽象,而抽象类是对根源的抽象。例如员工是抽象类,而部门经理、研发人员等是实现类,而员工的行为可以抽象为接口,如工作接口、休息接口、吃饭接口等。
  • 从语法来讲,抽象被类继承、接口被类实现,抽象类可以没有抽象方法,但一旦有抽象方法就必须是抽象类,接口则默认声明方法是 public abstract ,而成员变量默认是 public static final 。自 Java 8 以来,接口中可以包含默认方法default)和静态方法,这使得接口的功能更接近于抽象类。默认方法允许接口定义部分实现,而不破坏现有接口的契约。
  • 继承只能单一继承,而实现可以实现多个,所以抽象类成本要比接口大,接口扩展性更强。
  • 当希望类可以实现多个不相关的行为时,使用接口。例如,一个类可能既是一个 Movable(可以移动的)对象,也可以是 Serializable(可序列化的)对象,类可以实现这些不同的接口。
  • 当多个类有相同的基本行为,且需要共享这些行为的实现时,使用抽象类。例如,Animal 类可以提供 sleep() 的实现,所有动物都需要休眠,但不同的动物会有不同的声音。

问:异常类?继承结构?

所有异常都继承自Throwable,并分解为Error(错误)和Exception(异常)。

Throwable:

  • Error(错误):Error分支的类结构主要负责描述运行时系统的内部错误和资源耗尽错误。程序不应该抛出此类错误,出现此错误时除了告诉客户外还应该使程序安全的终止。

    • OutOfMemoryError:当 JVM 内存不足时抛出。
    • StackOverflowError:当程序调用堆栈溢出(如无限递归)时抛出。
    • VirtualMachineError:当 JVM 出现严重问题时抛出。
  • Exception(异常):Exception分为两个分支,RuntimeException受检异常

    • RuntimeException(运行时异常):由程序错误导致的异常
      • NullPointerException:当程序尝试访问空对象的属性或方法时抛出。
      • ArrayIndexOutOfBoundsException:当数组索引越界时抛出。
      • ArithmeticException:当数学运算异常(如除以零)时抛出。
      • ClassCastException:当对象强制转换失败时抛出。
      • IllegalArgumentException:当传递给方法的参数不合法时抛出。
      • 等等…
    • CheckedException(受检异常):程序本身没问题但因为IO等错误导致的异常。受检异常是编译时检查的异常,必须在方法中声明 throws 或用 try-catch 块处理,否则编译器会报错。
      • IOException:表示输入输出操作失败时的异常。
      • SQLException:表示数据库操作失败时的异常。
      • FileNotFoundException:表示文件未找到时的异常。
      • ClassNotFoundException:表示找不到类时的异常。
      • 等等…

Java语言规范将派生于Error和RuntimeException的异常称为非受查异常unchecked,其它异常成为受查异常checked。若子类重写了父类的方法,其声明的受查异常不能比父类声明异常更通用。

问:受检异常和运行时异常的区别?

  1. 首先所有异常都继承自Throwable,并分解为Error(错误)和Exception(异常)。
  2. Error分支的类结构主要负责描述运行时系统的内部错误和资源耗尽错误。程序不应该抛出此类错误,出现此错误时除了告诉客户外还应该使程序安全的终止。
  3. Exception分为RuntimeException(运行时异常)和CheckedException(受检异常)。
    • 受检异常是编译时检查的异常,必须在方法中声明 throws 或用 try-catch 块处理,否则编译器会报错。
    • RuntimeException(运行时异常则是由程序错误导致的异常

问:Java中的异常处理机制?

  1. 通过面向对象的思想设计,将异常分类处理,并且可以自定义异常类。
  2. 提供 try-catch-finally 语句来捕获和处理异常,保证程序不会因为异常而崩溃。
  3. 通过 throw 关键字来抛出异常,通过 throws 声明方法可能抛出的异常。

六. 文件与流

6.1 文件

问:说下如何操控文件?(输入输出)怎么关闭呢? 发生异常怎么办?

答:

  • 通过I/O流来对文件进行操作,为了确保流的关闭会在finally语句块中进行close。Java 7后增加了带资源的try语句(try-with-resources),try块退出后会自动关闭资源。这样即使发生异常,也能确保不会因为流未关闭而造成内存泄漏或一直占用文件的情况。
  • 最基本的文件读取类是 FileInputStreamFileReader,但通常推荐使用带缓冲的 BufferedReader 以提高效率。写入则可以通过 FileWriterBufferedWriter 实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class FileReadExample {
public static void main(String[] args) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 逐行读取并打印文件内容
}
} catch (IOException e) {
e.printStackTrace(); // 处理可能的 IO 异常
} finally {
// 确保资源关闭
try {
if (reader != null) {
reader.close(); // 关闭文件流,避免资源泄漏
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

public class FileWriteExample {
public static void main(String[] args) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter("output.txt"));
writer.write("Hello, World!"); // 向文件写入数据
writer.newLine(); // 写入换行符
writer.write("Java File I/O");
} catch (IOException e) {
e.printStackTrace(); // 处理可能的 IO 异常
} finally {
// 确保资源关闭
try {
if (writer != null) {
writer.close(); // 关闭文件流,避免资源泄漏
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

public class FileReadWithResources {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 逐行读取并打印文件内容
}
} catch (IOException e) {
e.printStackTrace(); // 处理异常
}

try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, World!"); // 写入文件
writer.newLine();
writer.write("Java File I/O");
} catch (IOException e) {
e.printStackTrace(); // 处理异常
}
}
}

问:如何提高文件读写的性能?如何处理大文件而不导致内存溢出?

  1. 使用缓冲流(BufferedReaderBufferedWriter)。与直接通过 FileInputStreamFileOutputStream 进行读写不同,缓冲流会先将数据存入内存中的缓冲区,减少实际的磁盘 I/O 操作,从而提高效率。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class BufferedFileReadWrite {
    public static void main(String[] args) {
    // 使用缓冲流提高文件读写性能
    try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
    BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {

    String line;
    while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    缓冲流减少了对磁盘的直接访问,通过在内存中累积数据后再一次性写入磁盘,减少了系统调用次数,提升了效率。

  2. 使用 NIO。提供了基于通道(Channel)和缓冲区(Buffer)的高效文件操作方式。NIO 的 FileChannel 是处理大文件时的理想选择,它允许文件的直接内存映射、随机读写和批量 I/O 操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class NIOFileCopy {
    public static void main(String[] args) throws IOException {
    try (FileChannel sourceChannel = new FileInputStream("input.txt").getChannel();
    FileChannel targetChannel = new FileOutputStream("output.txt").getChannel()) {

    sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
    }
    }
    }

    NIO 的 FileChannel 支持直接内存映射,这意味着大文件的部分可以直接映射到内存,从而减少了不必要的内存开销和复制。

  3. 分批读写(分块处理大文件)处理大文件时,不应一次性将整个文件加载到内存中,这可能会导致内存溢出。可以通过分批次读取或写入的方式来避免这一问题。每次处理文件的一部分内容,将其分块读取。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class LargeFileProcessing {
    public static void main(String[] args) {
    File inputFile = new File("large_input.txt");
    long fileSize = inputFile.length();
    int bufferSize = 8 * 1024; // 8KB 缓冲区大小
    byte[] buffer = new byte[bufferSize];

    try (InputStream in = new FileInputStream(inputFile)) {
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
    // 处理数据,例如写入到另一个文件中
    processBuffer(buffer, bytesRead);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    private static void processBuffer(byte[] buffer, int length) {
    // 处理缓冲区的数据
    System.out.println("Processing " + length + " bytes");
    }
    }
  4. 内存映射文件(Memory Mapped File)是 NIO 中的高级功能,它允许将文件的一部分或全部直接映射到内存中。这种方式对于处理大文件和高性能 I/O 是非常高效的,尤其适用于随机访问大文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MemoryMappedFileExample {
    public static void main(String[] args) throws Exception {
    RandomAccessFile file = new RandomAccessFile("large_input.txt", "rw");
    FileChannel channel = file.getChannel();

    // 将文件的前 1GB 映射到内存
    MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024 * 1024);

    // 读取文件内容
    for (int i = 0; i < buffer.limit(); i++) {
    byte b = buffer.get(i);
    // 处理数据...
    }

    file.close();
    }
    }
  5. 多线程读写:对于非常大的文件,可以通过多线程并发读写来进一步提升性能。将文件分成多个块,每个块由不同的线程并发处理。需要注意的是,文件的多线程写入操作可能需要额外的同步机制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class MultiThreadFileReader {
    public static void main(String[] args) {
    // 使用多个线程分别处理文件的不同部分
    int numberOfThreads = 4; // 假设使用4个线程
    File file = new File("large_input.txt");
    long fileSize = file.length();
    long partSize = fileSize / numberOfThreads;

    for (int i = 0; i < numberOfThreads; i++) {
    long start = i * partSize;
    long end = (i == numberOfThreads - 1) ? fileSize : (i + 1) * partSize;
    new Thread(new FileReadTask(file, start, end)).start();
    }
    }
    }

    class FileReadTask implements Runnable {
    private File file;
    private long start;
    private long end;

    public FileReadTask(File file, long start, long end) {
    this.file = file;
    this.start = start;
    this.end = end;
    }

    @Override
    public void run() {
    try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
    raf.seek(start);
    byte[] buffer = new byte[8 * 1024]; // 8KB缓冲区
    int bytesRead;
    long totalBytesRead = 0;
    while ((bytesRead = raf.read(buffer)) != -1 && totalBytesRead < (end - start)) {
    totalBytesRead += bytesRead;
    // 处理数据
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

问:文件的序列化和反序列化?如何在 Java 中将对象序列化到文件中?如何从文件中反序列化对象?

问:讲讲序列化?

对象序列化

  • 序列化指将一个Java对象转化为二进制序列,可以看作一个 byte[] 字节数组。Java序列化基于两个接口:Serializable和Externalizable,Serializable接口有默认序列化机制。
  • 文件的序列化和反序列化?序列化和反序列化是将对象状态转换为字节流的过程,以及将字节流恢复为对象状态的过程。这通常用于将对象持久化到磁盘或通过网络传输对象。
    • 序列化(Serialization)是将对象的状态保存到字节流中,这样可以将对象存储到文件中、数据库中,或通过网络传输。必须让对象的类实现 java.io.Serializable 接口。然后使用 ObjectOutputStream 写对象到文件。
    • 反序列化(Deserialization)是将字节流恢复为对象状态的过程。这涉及到从文件或其他输入源中读取字节流,并将其转换回原始对象。反序列化过程要求对象的类与序列化时相同,并且实现了 Serializable 接口。使用 ObjectInputStream 从文件中读取对象
  • 如何在 Java 中将对象序列化到文件中?如何从文件中反序列化对象?
    • 分别使用 ObjectOutputStreamObjectInputStream
  • 注意事项:
    1. **serialVersionUID**:
      • serialVersionUID 是一个版本控制标识符,用于确保序列化和反序列化过程中的兼容性。
      • 如果类的 serialVersionUID 不匹配,将会抛出 InvalidClassException 异常。
    2. transient 关键字
      • 使用 transient 关键字修饰的字段在序列化过程中会被忽略,即这些字段的值不会被写入到字节流中。
      • 反序列化时,这些字段的值将被初始化为默认值。
    3. 可序列化的对象
      • 序列化的对象必须实现 Serializable 接口。
      • 序列化对象的字段也必须是可序列化的;如果字段是不可序列化的,可以标记为 transient
    4. 性能和安全
      • 反序列化操作可能会带来安全风险,特别是在处理来自不信任来源的数据时。
      • 序列化和反序列化可以影响性能,特别是对于大对象或复杂对象图。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
javaCopy codeimport java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Person implements Serializable {
private static final long serialVersionUID = 1L; // 推荐定义
private String name;
private transient int age; // transient 关键字表示此字段不会被序列化

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}

public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("John", 30);

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
oos.writeObject(person); // 序列化对象到文件
} catch (IOException e) {
e.printStackTrace();
}
}
}


public class DeserializationExample {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person person = (Person) ois.readObject(); // 从文件中反序列化对象
System.out.println(person); // 输出反序列化后的对象
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

IO流

问:讲讲你所知道的IO流?

  1. 根据传输的基本单位分为字节流字符流,字节流一般以Stream命名,字符流则以Reader/Writer命名。
  2. 字节流用来处理字节(字节流一次只能操作一个字节,适合处理二进制文件)。字符流用来处理字节流不方便处理的Unicode格式(字符流以字符为单位处理数据,适用于处理文本文件)。
  3. 根据读出或写入一个字节序列可以把字节流分为输出流和输入流,流处理的对象可以是文件、网络连接或内存块。
  • 字节输入流(InputStream):
    • FileInputStream:从文件中读取字节数据。
    • BufferedInputStream:为输入流提供缓冲功能,提高读取性能。
    • ByteArrayInputStream:从字节数组中读取数据。
    • ObjectInputStream:用于读取序列化对象。
  • 字节输出流(OutputStream):
    • FileOutputStream:将字节数据写入文件。
    • BufferedOutputStream:为输出流提供缓冲功能,提高写入性能。
    • ByteArrayOutputStream:将数据写入字节数组中。
    • ObjectOutputStream:用于序列化对象并写入流中。
  • 字符输入流(Reader):
    • FileReader:从文件中读取字符数据。
    • BufferedReader:为字符输入流提供缓冲功能,并提供按行读取的功能。
    • InputStreamReader:将字节输入流转换为字符输入流,通常用于处理带有编码的字节数据。
  • 字符输出流(Writer):
    • FileWriter:将字符数据写入文件。
    • BufferedWriter:为字符输出流提供缓冲功能,支持批量写入。
    • OutputStreamWriter:将字节输出流转换为字符输出流,通常用于处理带有编码的字节数据。

问:讲讲字符流和字节流的区别?

  • 处理对象不同,虽然本质上计算机传输都是字节,但字符流提供了对Unicode的直接操作,字节流则只能处理字节。
  • 字节流操作时不会用到缓冲区,直接在文件操作,而字符流则会先在缓冲区上操作,在关闭时再强制性的把缓冲区内容输出,缓冲区其实就是一块特殊的内存区域,读取速度要远远高于读取磁盘的速度。
  • 字符流通过字节流来实现。

NIO

问:如何理解同步异步?阻塞与非阻塞?

理解这些概念要结合相应的语境,一般主要在进程通信I/O系统调用方面讨论这些概念。

  • 什么是进程通信?

    • 进程间通信由send()和receive()两种动作完成,消息的传递可能是阻塞非阻塞的,也可以叫做同步异步的,此处二者指相同概念。指进程访问数据时,数据是否准备就绪。
  • 发送和接收可以根据阻塞分为?

    • 阻塞式发送(blocking send):发送方进程会被一直阻塞, 直到消息被接受方进程收到。
    • 非阻塞式发送(nonblocking send): 发送方进程调用 send() 后, 立即就可以其他操作。
    • 阻塞式接收(blocking receive):接收方调用 receive() 后一直阻塞, 直到消息到达可用。
    • 非阻塞式接受(nonblocking receive): 接收方调用 receive() 函数后, 要么得到一个有效的结果,要么得到一个空值, 即不会被阻塞。
  • 什么是阻塞?

    • 阻塞是指进程在发起一个系统调用(System Call)后,数据还未就绪,等待调用操作完成时,内核将进程挂起为等待(waiting)状态,以确保它此时不会被调度执行。
    • I/O System Call:阻塞与系统调用紧密相关,现代计算机中物理通信操作通常是异步的,而操作系统则默认提供阻塞式的I/O系统调用接口(blocking systemcall),这样使应用代码编写更简单(代码执行顺序与编写顺序一致)。当然也会提供非阻塞式的I/O系统调用接口(nonblocking systemcall),不会挂起程序,而是立刻返回一个值。
  • IO操作包括哪两个阶段?

    • 1.查看数据是否就绪;
    • 2.进行数据拷贝,内核将数据拷贝到用户线程。
  • 同步异步主要针对用户线程和内核交互

    • 阻塞式I/O系统调用:进程发起系统调用后挂起,直到调用操作完成。
    • 非阻塞式I/O系统调用:进程发起系统调用后立刻返回一个值,而不会挂起。
    • 同步I/O系统调用:用户线程发起I/O请求后,用户线程或内核不断轮询数据是否就绪,就绪后将数据从内核拷贝回用户线程。
    • 异步I/O系统调用:用户线程发起I/O请求,之后都由内核完成,完成后通知用户线程。与非阻塞式的I/O系统调用类似的是,二者不会等待I/O操作完成,应用程序可以继续执行其他操作,待I/O完成时操作系统通知调用进程。二者的区别是非阻塞会立即返回数据(无论数据是否完整),而异步则要求结果是完整的,允许延迟获取。

来源:怎样理解阻塞非阻塞与同步异步的区别?萧萧

问:同步I/O和异步I/O的区别?

  • 同步IO用户线程会阻塞,数据拷贝阶段由用户线程完成。
  • 异步IO用户线程不会阻塞,数据拷贝阶段由内核完成,且会延迟等待完整结果。

问:谈谈5种IO模型?

  • 有哪5种IO模型?
    • 阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO。
  • 这5种IO模型哪些是同步的,哪些是异步的?
    • 前四种都是同步IO,只有异步IO是真正的异步式IO,因为前四种都至少会在IO操作的第二个阶段使用户线程阻塞,且前四种只有第一阶段不同,第二阶段都是阻塞拷贝。
    • 阻塞IO:即读写操作都需要等待;
    • 非阻塞IO:则是当数据未就绪时返回一个错误信息,用户线程持续轮询;
    • IO多路复用:用一个线程就可以管理多个socket,同时检测多个资源,但线程会阻塞直到有资源可用,轮询也是通过内核而非用户线程执行;
    • 信号驱动IO:在数据未就绪时不会阻塞,等接收到就绪信号时再通过信号函数调用IO读写操作;
    • 异步IO:用户线程发起请求后立刻可以去做其它操作,内核收到信号后也立刻返回。后续由内核完成数据拷贝到用户线程,最终通知用户线程。两个阶段都由内核完成。

问:什么是I/O多路复用?相比非阻塞I/O为何效率更高?

  • 什么是IO多路复用?
    • 通过一个线程同时监控多个Socket,当某个Socket就绪时,线程执行IO操作,无需为每个Socket创建一个线程。
  • 相比非阻塞I/O为何效率更高?
    • 多个Socket只需一个线程管理,只有真正进行读写操作时才会使用IO资源。
    • 非阻塞的轮询由用户线程来做,而多路复用由内核来轮询Socket状态,用户线程处于阻塞状态。

问:知道NIO吗?对比IO简单谈一下区别?Java BIO是什么?

  • 什么是BIO?

    • BIO即Blocking IO,是一个同步并阻塞的IO模式,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
  • 什么是NIO?

    • NIO即New IO,NIO 是一种同步非阻塞的 I/O 模型,实际上是一种多路复用IO。在JDK 1.4时引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。
    • 它支持面向缓冲的,基于通道的 I/O 操作方法。NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
    • IO主要包括类File,outputStream,inputStream,writer,reader,seralizable(5类1接口)
    • NIO则包含三大核心内容 selector(选择器,用于监听channel),channel(通道),buffer(缓冲区)。NIO选择器允许一个单独的线程监视多个输入通道。数据则无论读写都是放入缓冲区来处理。通道则用于在文件/Socket和缓冲区之间传输数据。传统Stream是单向的,而Channel则是双向。
  • IO与NIO的区别有?二者有相同的作用但实现方式不同。NIO效率要比IO高很多。

    • IO是面向流设计,NIO则面向缓冲。因为IO面向流,所以意味着每次直接从流读取字节直到读取完毕。NIO因为面向缓冲,可以将缓冲区的数据任意移动位置。
    • IO属于阻塞式IO,NIO则属于非阻塞式IO。IO的流是阻塞的,这意味着当一个线程读写数据时,此线程不能同时处理其他事情。而NIO会让线程从通道获取当前可用的数据,当数据还未能读取或写入前,线程可以去做其他事情。线程通常会将在非阻塞式IO上的空闲时间用于去其他通道执行IO操作,所以一个线程可以管理多个输入和输出通道。
IO模型 BIO NIO
通信 面向流 面向缓冲
处理 阻塞 IO 非阻塞 IO
触发 选择器

NIO的特点:

  1. 一个线程可以处理多个通道,减少线程创建数量;
  2. 读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费

问:什么是 Reactor 模型? Proactor与Reactor?

  • 什么是 Reactor 模型?
    • IO复用需要事件分发器(event dispatcher),将读写事件分发给各个处理者Handler。事件分发有两种模式:ReactorProactor
    • Reactor模式:将IO操作分为事件检测和事件处理两阶段,通过一个或多个 Reactor 对象来检测 I/O 事件,并将这些事件分派给相应的事件处理器(Handler)进行处理。
    • Proactor模式:Proactor直接发起一个异步 I/O 操作,实际工作由操作系统完成,I/O 操作完成后,通知 Proactor。Proactor 再调用相应的 Handler处理完成的IO事件。
  • Proactor与Reactor?
    • 二者都是事件驱动的设计模式。
    • Reactor的IO操作由Handler完成,Proactor则由操作系统完成。
    • 当回调Handler时,Proactor异步表示IO操作已完成,Reactor则表示可以进行IO操作。

问:如何使用 FileChannel 进行文件读写?如何使用 MappedByteBuffer 实现内存映射文件?如何使用 NIO 的异步 I/O 操作? 如何使用 java.nio.file 包?如何使用 Files 类读取和写入文件?如何使用 Path 类处理文件路径?

  1. 使用 FileChannel 进行文件读写:允许我们更高效地进行文件的读写操作。相比传统的 FileInputStreamFileOutputStreamFileChannel 支持随机访问文件、部分读写以及映射文件到内存等高级操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class FileChannelReadExample {
    public static void main(String[] args) throws Exception {
    // 使用 RandomAccessFile 打开文件通道
    RandomAccessFile file = new RandomAccessFile("example.txt", "r");
    FileChannel fileChannel = file.getChannel();

    // 创建一个 ByteBuffer 来存储数据
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    // 读取数据到缓冲区
    int bytesRead = fileChannel.read(buffer);
    while (bytesRead != -1) {
    buffer.flip(); // 切换读模式

    while (buffer.hasRemaining()) {
    System.out.print((char) buffer.get());
    }

    buffer.clear(); // 清空缓冲区,准备下一次读取
    bytesRead = fileChannel.read(buffer);
    }
    fileChannel.close();
    file.close();
    }
    }

    public class FileChannelWriteExample {
    public static void main(String[] args) throws Exception {
    // 打开一个文件,准备写入
    RandomAccessFile file = new RandomAccessFile("output.txt", "rw");
    FileChannel fileChannel = file.getChannel();

    // 准备写入的数据
    String data = "Hello, FileChannel!";
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put(data.getBytes());

    buffer.flip(); // 切换到读模式
    fileChannel.write(buffer); // 将缓冲区中的数据写入文件

    fileChannel.close();
    file.close();
    }
    }
  2. 使用 MappedByteBuffer 实现内存映射文件:可以将文件映射到内存中,这样就可以像操作内存一样操作文件。它允许直接访问大文件中的特定部分而不必一次性加载整个文件,适合处理大文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class MappedByteBufferExample {
    public static void main(String[] args) throws Exception {
    // 打开文件通道
    RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
    FileChannel fileChannel = file.getChannel();

    // 将文件的一部分映射到内存中
    MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());

    // 读取文件内容
    while (buffer.hasRemaining()) {
    System.out.print((char) buffer.get());
    }

    // 写入文件内容
    buffer.put(0, (byte) 'H'); // 修改文件的第一个字节
    fileChannel.close();
    file.close();
    }
    }
  3. 使用 NIO 的异步 I/O 操作:通过 AsynchronousFileChannel 可以实现非阻塞的文件操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class AsyncFileReadExample {
    public static void main(String[] args) throws IOException {
    AsynchronousFileChannel asyncFileChannel = AsynchronousFileChannel.open(
    Paths.get("example.txt"), StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    Future<Integer> result = asyncFileChannel.read(buffer, 0);

    while (!result.isDone()) {
    System.out.println("Reading file asynchronously...");
    }

    buffer.flip();
    while (buffer.hasRemaining()) {
    System.out.print((char) buffer.get());
    }
    asyncFileChannel.close();
    }
    }
  4. 如何使用 java.nio.file 包?如何使用 Files 类读取和写入文件?如何使用 Path 类处理文件路径?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    public class FilesReadExample {
    public static void main(String[] args) throws Exception {
    List<String> lines = Files.readAllLines(Paths.get("example.txt"));
    for (String line : lines) {
    System.out.println(line);
    }
    }
    }

    public class FilesWriteExample {
    public static void main(String[] args) throws Exception {
    Files.write(Paths.get("output.txt"), Arrays.asList("Hello, World!", "Java NIO is great!"));
    }
    }

    public class PathExample {
    public static void main(String[] args) {
    Path path = Paths.get("example.txt");

    System.out.println("File Name: " + path.getFileName());
    System.out.println("Parent Directory: " + path.getParent());
    System.out.println("Absolute Path: " + path.toAbsolutePath());
    }
    }

七. 反射与动态代理

问:反射的原理,怎么确定类,怎么调方法?

  • Java 反射机制是一种运行时动态获取类信息和操作对象的能力,它允许在运行时确定类和方法,并且可以动态地调用方法和访问字段。对于类,反射可以获取类的完整信息,对于对象可以调用任意属性和方法。

  • 当 JVM 加载一个类时,会在方法区为该类生成一个 Class 对象,这个对象封装了该类的所有信息(如类的名称、包的名称、方法、字段等)。获取类的字节码对象(Class)的三种方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 1. 使用类的全限定名
    Class<?> cls1 = Class.forName("com.example.MyClass");

    // 2. 通过对象的 `getClass()` 方法
    MyClass myObject = new MyClass();
    Class<?> cls2 = myObject.getClass();

    // 3. 直接通过类名的 .class 属性
    Class<?> cls3 = MyClass.class;
  • 通过 Class 对象获取类的构造方法,从而动态创建对象:

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取所有的构造方法
    Constructor<?>[] constructors = cls1.getConstructors();

    // 获取指定参数类型的构造方法
    Constructor<?> constructor = cls1.getConstructor(String.class, int.class);

    // 使用构造方法创建对象
    Object obj = constructor.newInstance("Alice", 25);
  • 通过 getDeclaredFields() 方法获取类的字段(包括私有字段),并可以对字段进行操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 获取所有字段,包括私有字段
    Field[] fields = cls1.getDeclaredFields();

    // 获取指定的字段
    Field field = cls1.getDeclaredField("name");

    // 设置私有字段的可访问性
    field.setAccessible(true);

    // 获取字段的值
    Object value = field.get(obj);

    // 设置字段的值
    field.set(obj, "Bob");
  • 通过 getDeclaredMethods() 方法获得类的所有方法, getDeclaredMethod(String) 方法获取指定方法名的方法,并通过 Method.invoke() 调用此方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 获取所有方法
    Method[] methods = cls1.getMethods(); // 获取包括父类和接口的公共方法

    // 获取所有声明的方法(包括私有方法)
    Method[] declaredMethods = cls1.getDeclaredMethods();

    // 获取指定的方法
    Method method = cls1.getMethod("sayHello", String.class);

    // 调用方法
    Object result = method.invoke(obj, "Hello");

问:如何通过反射和设置对象私有字段的值?

  • 首先使用反射提供的setAccessible(true),使我们可以访问到非public的变量。
  • 然后使用反射提供的getDeclaredFields()方法获取某个类的所有声明字段或getDeclaredField()方法获取指定字段。

问:动态代理?

  • 什么是动态代理?
    • Java 动态代理是一种设计模式,它允许在运行时动态生成代理类,用于代理目标对象的方法调用。
  • 与静态代理的区别?
    • 动态代理指代理类在运行时创建,而非编译阶段生成的代理模式。解决了静态代理存在的代码冗余等问题,动态生成代理对象,用完即销毁。
    • 静态代理:需要为每一个目标类编写一个代理类,编译时就确定了代理类,增加了代码量和维护成本。
    • 动态代理:不需要手动编写代理类,而是通过 Java 反射机制在运行时动态生成代理对象。
  • 动态代理基于 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来实现。它通过实现接口的代理类来控制对目标对象方法的调用。
    • Proxy:用于动态生成代理类的类。通过 Proxy.newProxyInstance() 方法,Java 可以在运行时创建一个代理对象,而不需要事先编写代理类。
    • InvocationHandler 接口:定义了代理类方法调用时的处理逻辑。

动态代理步骤:

  1. 获取 RealSubject上的所有接口列表;
  2. 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX
  3. 根据需要实现的接口信息,在代码中动态创建该 Proxy 类的字节码;
  4. 将对应的字节码转换为对应的 Class 对象;
  5. 创建 InvocationHandler 实例 handler,用来处理 Proxy 所有方法调用;
  6. ProxyClass 对象以创建的 handler 对象为参数,实例化一个 proxy 对象。

代理类命名格式固定,一旦在运行中创建则和普通类无异。代理类都继承自Proxy类,通过实现接口的方式来实现代理。

使用示例:

  1. 创建接口和目标对象

    首先,定义一个接口及其实现类。假设有一个 HelloService 接口及其实现类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface HelloService {
    void sayHello(String name);
    }

    public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello(String name) {
    System.out.println("Hello, " + name);
    }
    }
  2. 实现 InvocationHandler 接口

    InvocationHandler 接口定义了代理类的方法调用逻辑,它需要实现 invoke() 方法,代理类的所有方法调用都会被转发到 invoke() 方法中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;

    public class HelloServiceInvocationHandler implements InvocationHandler {
    private Object target;

    public HelloServiceInvocationHandler(Object target) {
    this.target = target; // 目标对象
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 在方法执行之前可以添加一些自定义逻辑
    System.out.println("Before method: " + method.getName());

    // 调用目标对象的方法
    Object result = method.invoke(target, args);

    // 在方法执行之后可以添加一些自定义逻辑
    System.out.println("After method: " + method.getName());

    return result;
    }
    }
  3. 生成代理对象并使用

    通过 Proxy.newProxyInstance() 方法创建代理对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import java.lang.reflect.Proxy;

    public class DynamicProxyDemo {
    public static void main(String[] args) {
    // 创建目标对象
    HelloService helloService = new HelloServiceImpl();

    // 创建 InvocationHandler 实例,将目标对象传入
    HelloServiceInvocationHandler handler = new HelloServiceInvocationHandler(helloService);

    // 通过 Proxy 动态生成代理对象
    HelloService proxy = (HelloService) Proxy.newProxyInstance(
    helloService.getClass().getClassLoader(), // 类加载器
    helloService.getClass().getInterfaces(), // 目标对象实现的接口
    handler); // InvocationHandler 实例

    // 通过代理对象调用方法
    proxy.sayHello("World");
    }
    }
  4. 输出结果:

    1
    2
    3
    plaintextCopy codeBefore method: sayHello
    Hello, World
    After method: sayHello

动态代理常用于以下场景:

  • AOP(面向切面编程):可以在方法执行前后添加横切逻辑,如日志、权限验证、事务管理等。
  • 远程调用代理:在分布式系统中,动态代理常用于创建远程服务的代理对象,简化远程调用的复杂性。
  • 权限验证:在方法执行之前动态检查用户权限。
  • 事务管理:动态代理可以在方法执行之前和之后进行事务的开启、提交或回滚。

问:为什么JDK动态代理只能代理接口?

  • 因为JDK动态代理创建代理对象需要继承标准类库中的 Proxy 类, Proxy 类是通过接口来创建代理对象的,它需要代理类实现某些接口。
  • 且Java遵守单继承多实现,所以JDK只能通过实现来代理接口。

通过CGLIB 代理可以代理没有实现接口的类。

问:有人说jdk动态代理性能比cglib要差,如果是,依据是什么?JDK 动态代理与 CGLIB 代理的区别又是什么?

二者的区别:

  • JDK 动态代理:只能代理实现了接口的类,基于接口生成代理类。
    • 基于反射机制,JDK 动态代理会在运行时生成代理类,并且代理类实现目标对象的接口。代理类的每个方法调用都会通过 InvocationHandlerinvoke 方法来转发。
    • 基于 Java 的反射机制,代理对象实际上是实现了接口的一个代理类,调用方法时通过反射来执行。
  • CGLIB 代理:可以代理没有实现接口的类,基于继承目标类生成代理类,通过字节码操作生成代理类。
    • CGLIB 使用底层字节码生成技术(ASM)在运行时生成目标类的子类,并通过方法覆盖的方式实现方法拦截。
    • 通过继承目标类,代理类会覆盖目标类的非 final 方法,并在代理时通过字节码生成技术(如 ASM)来动态生成新的代理类。

JDK 动态代理的局限性

  • 只能代理实现了接口的类,不能代理没有接口的普通类。
  • 代理类在每次调用方法时,都会通过反射来执行目标对象的方法,性能稍低。

CGLIB 动态代理的优势

  • CGLIB 通过继承目标类生成代理类,因此即使没有接口也能实现代理。
  • CGLIB 直接生成目标类的子类,调用方法时无需反射,性能更高。
  1. 说jdk动态代理性能比cglib要差,依据应该是JDK动态代理依赖于反射机制,带来了性能开销,而CGLIB则通过ASM操作字节码直接生成类,且调用方法时性能接近于直接调用,而JDK则需要反射机制进行中转。
  2. 但在JDK 1.8之前的版本或许JDK动态代理生成的代理类运行效率要弱于CGLib,但之后的版本JDK明显优化过后应该区别不大。
  3. 1.8版本的改进:引入方法句柄(MethodHandles)替代传统反射机制;支持动态语言的调用 invokedynamic 指令优化了方法调用的性能。

问:Spring中AOP的实现采用那种代理方式?

  • Spring虽然采用了AspectJ的标准注解,但未采用其静态代理的模式(特定的编译器和语法在编译阶段实现代理),而是动态代理。
  • 其支持的动态代理实现方式有两种:JDK动态代理和CGLib,Spring会默认使用JDK动态代理当目标对象非接口时会强制转换为CGLib。Spring Boot则在2.x版本后默认使用CGlib(理由是相比JDK动态代理更少出现转换问题)。可以通过在 Spring 的配置中设置 proxyTargetClass=true 来实现强制使用 CGLIB 代理。

问:Java中的事件机制?

  1. Java 中的事件机制是一种基于观察者模式(Observer Pattern)的模型,允许对象之间通过事件进行解耦和通信。

  2. 核心概念:

    • 事件对象(Event Object):一般继承自 java.util.EventObject 类,封装了事件源对象及跟事件相关的信息。
    • 事件源(Event Source):事件发生的地方,由于事件源的某项属性或状态发生了改变(比如 BUTTON 被单击、TEXTBOX 的值发生改变等等)导致某项事件发生。换句话说就是生成了相应的事件对象。因为事件监听器要注册在事件源上,所以事件源类中应该要有盛装监听器的容器(List,Set 等等)。
    • 事件监听器(Event Listener):实现 java.util.EventListener 接口,注册在事件源上,当事件源的属性或状态改变时,取得相应的监听器调用其内部的回调方法。 Java 的事件监听器通常是接口,如 ActionListenerMouseListenerKeyListener 等。
    • 事件处理(Event Handling):当事件发生时,事件源会调用所有注册的事件监听器的处理方法,触发相应的处理逻辑。
  3. 流程:

    1. 定义事件监听器接口:Java 提供了多个事件监听器接口,例如:

      • ActionListener:处理按钮、菜单等的点击事件。
      • MouseListener:处理鼠标事件,如点击、进入、离开等。
      • KeyListener:处理键盘事件。
    2. 注册事件监听器:通过事件源的 addXXXListener 方法来注册事件监听器。例如:

      1
      2
      3
      4
      5
      6
      JButton button = new JButton("Click Me");
      button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      System.out.println("Button clicked!");
      }
      });
    3. 事件触发:当用户点击按钮时,事件源触发 ActionEvent,然后调用注册的 ActionListeneractionPerformed 方法。

八. 设计模式

问:项目中有使用过哪些设计模式?

  • 单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点。例如在整个系统中共享资源,如数据库连接池、配置管理类等。
  • 工厂模式(Factory Pattern)创建对象时不直接实例化对象,而是通过工厂方法创建,便于管理和扩展。常用于解耦对象创建与使用。比如根据不同的订单类型创建不同的订单处理类。
  • 观察者模式(Observer Pattern)当一个对象的状态改变时,所有依赖它的对象都能得到通知并自动更新。主要用于事件驱动的系统。比如消息推送系统
  • 适配器模式(Adapter Pattern)将一个类的接口转换为用户所期望的另一个接口。它的目的是通过适配器类将两个不兼容的接口结合在一起,从而使得原本因接口不兼容而无法一起工作的类能够协同工作。比如我们系统支持某个旧的视频播放器,需要支持新的格式,为了不修改原有代码,可以使用该模式。
  • 代理模式(Proxy Pattern)为其他对象提供代理以控制对这个对象的访问。常用于远程代理、虚拟代理、保护代理等。比如事务管理、日志记录。
  • 模板方法模式(Template Method Pattern)定义算法的骨架,而将某些步骤延迟到子类中实现。模板方法允许子类在不改变算法结构的情况下,重新定义算法的某些步骤。在电商项目中,订单处理的流程大致相同,但具体的支付方式或物流方式有所不同,使用模板方法可以统一流程,同时允许不同子类扩展具体实现。
  • 策略模式(Strategy Pattern)定义一系列算法,将每个算法封装起来,使它们可以相互替换。这种模式让算法独立于使用它的客户端。比如支付系统中定义不同的支付方式。
  • 责任链模式(Chain of Responsibility Pattern)将多个处理器串联起来,形成一个处理链,处理链中的每个处理器负责处理不同的任务。常用于日志处理、权限校验等场景。在权限系统中,不同的角色有不同的权限,通过责任链模式可以将不同的权限处理逻辑组合在一起,按顺序进行处理。
  • 装饰者模式(Decorator Pattern)动态地为对象增加新的功能,而不影响其结构。适合在不修改现有类的情况下扩展功能。

问: JDK 中几个常见的设计模式?

  • 单例模式:用于 Runtime,Calendar 和其他的一些类中
    • 如通过 Runtime.getRuntime() 获取单例实例。
  • 工厂模式:被用于各种不可变的类如 Boolean,像 Boolean.valueOf。
    • java.util.Calendar.getInstance() 使用了工厂模式创建 Calendar 对象。
    • java.sql.DriverManager.getConnection() 也是工厂模式的应用,用于获取数据库连接。
  • 策略模式:
    • java.util.Comparator 就是典型的策略模式,用户可以定义不同的排序策略传递给 Collections.sort() 方法。
    • javax.crypto.Cipher 使用不同的加密策略。
  • 观察者模式:被用于 Swing 和很多的事件监听中。
    • java.util.Observerjava.util.Observable 使用观察者模式实现数据更新。
    • GUI 组件的事件监听器也是观察者模式的实现,例如 ActionListener
  • 装饰器模式: 被用于 Java IO 中的各种字节和字符流,InputStream和XXInputStream,如BufferedReader和BufferedWriter,增强了 Reader和Writer,通过Buffer读写提高性能。
    • java.io.BufferedReaderjava.io.FileReader 是装饰器模式的经典应用,BufferedReaderReader 对象增加了缓冲功能。
    • java.util.Collections.synchronizedList()List 对象添加线程安全的职责。
  • 迭代器模式:java.util.Iterator
  • 代理模式:java.lang.reflect.Proxy

问: Spring 中用了哪些设计模式?

  • 单例模式:Spring 的默认 Bean 作用域是 Singleton,即每个 Spring 容器中一个 Bean 只有一个实例。这意味着通过 @Bean 或 XML 配置定义的 Bean 在容器中都是单例的。
  • 简单工厂模式:如BeanFactory等。
  • 工厂模式:AbstractFactoryBean。
  • 代理模式:
    • Spring AOP(面向切面编程)使用了代理模式。使用 @Transactional 或其他 AOP 注解时,Spring 会通过代理模式为目标方法添加事务管理。
  • 适配器模式:
    • Spring 的 HandlerAdapter 就是典型的适配器模式,它使得不同类型的控制器(如 @ControllerSimpleController)可以被 DispatcherServlet 处理。
  • 装饰器模式:
    • **HttpServletRequestWrapperHttpServletResponseWrapper**:这两个类是 Java Servlet API 的一部分,而 Spring 中也经常用到。它们允许在不改变原始请求或响应对象的情况下,扩展请求和响应的功能。
  • 观察者模式:
    • Spring 的事件驱动模型基于观察者模式。通过 ApplicationEvent 和 **ApplicationListener**,Spring 提供了事件发布和监听机制。ApplicationContext 会作为事件源,触发事件,监听者会收到通知并处理。
  • 策略模式:
    • **SpringConversionService**:Spring 的类型转换服务是策略模式的典型应用。ConversionService 定义了通用的接口,可以使用不同的策略来执行类型转换。Spring 提供了不同的 Converter 实现,用于在不同类型之间进行转换,而开发者也可以自己定义新的 Converter
    • Spring 的事务管理:Spring 的事务管理使用策略模式定义了不同的事务传播行为(如 REQUIREDREQUIRES_NEWSUPPORTS 等),通过 @Transactional 注解或编程方式,用户可以选择合适的事务策略。
    • BeanFactory 的后置处理器:当 Spring 容器初始化时,BeanFactoryPostProcessorBeanPostProcessor 可以定义不同的处理策略,来在 Bean 创建的不同生命周期中定制处理逻辑。
  • 模板方法模式:
    • Spring 提供了一些模板类,如 **JdbcTemplateRestTemplate**,这些类封装了常见操作的流程,开发者只需定义具体的回调操作
  • 责任链模式 (Chain of Responsibility Pattern):
  • Spring 的拦截器机制(HandlerInterceptor)使用了责任链模式。当请求到达时,多个拦截器可以按顺序处理请求,直到最终目标处理器。
  • 依赖注入模式 (Dependency Injection Pattern):Spring 的核心理念就是 **依赖注入 (DI)**,通过 @Autowired@Inject 等注解,Spring 自动注入依赖的 Bean。

问:单例模式是什么?单例模式的优点?使用场景?有哪几种?有什么区别?手写出线程安全的单例模式?

  • 什么是单例模式?

    • 通过该模式创建的类只能有一个实例,且该实例可以被全局访问
    • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例。
    • 没有公开的set方法,外部类无法调用set方法创建该实例。
    • 提供一个公开的get方法获取唯一的这个实例。
  • 单例模式的优点?

    • 避免了对象的重复创建,减轻GC压力。
    • 保证系统中某个类的实例对象是唯一的,可以通过全局的访问点访问该实例。
  • 使用场景?

    • 日志记录类:日志记录系统通常要求全局唯一的访问点,使用单例模式可以避免多个日志类实例。
    • 数据库连接池:避免频繁创建和销毁数据库连接,保证资源的有效使用。
    • 线程池管理:通过单例模式管理线程池,避免创建多余的线程池实例。
    • 配置文件读取类:确保应用程序读取配置文件的类全局唯一。
  • JDK中的使用案例:用于 Runtime,Calendar 和其他的一些类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Runtime {
    // 饿汉式
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
    return currentRuntime;
    }
    }

    // 工厂模式
    public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {

    public static Calendar getInstance()
    {
    return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }

    // 懒汉式线程不安全
    private static Calendar createCalendar(TimeZone zone,
    Locale aLocale)
    {
    CalendarProvider provider =
    LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
    .getCalendarProvider();
    // 省略
    Calendar cal = null;

    // 省略
    return cal;
    }
    }
  • Spring中的使用案例:Spring 的默认 Bean 作用域是 Singleton,即每个 Spring 容器中一个 Bean 只有一个实例。这意味着通过 @Bean 或 XML 配置定义的 Bean 在容器中都是单例的。

    • Spring使用单例注册表来实现单例模式,单例注册表(Singleton Registry)是通过DefaultSingletonBeanRegistry这个类来实现的。其是负责管理单例bean的默认实现。它维护了一个单例缓存(通过ConcurrentHashMap实现的singletonObjects)来保存已经创建的单例bean实例,以及一个早期单例缓存(earlySingletonObjects)来保存在bean的实例化过程中早期暴露的bean实例。
  • 分为哪几种?

    • 饿汉式:提前创建对象,首次获取即有对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class Singleton {
      // 创建一个实例对象
      private static Singleton instance = new Singleton();
      /**
      * 私有构造方法,防止被实例化
      */
      private Singleton(){}
      /**
      * 静态get方法
      */
      public static Singleton getInstance(){
      return instance;
      }
      }
    • 懒汉式:首次调用时,对象是空的,然后再去初始化赋值。需要考虑线程安全问题。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64

      public class Singleton {
      private static Singleton instance;
      private Singleton (){}

      // 线程不安全
      public static Singleton getInstance() {
      if (instance == null) {
      instance = new Singleton();
      }
      return instance;
      }

      // 通过volatile修饰的变量,不会被线程本地缓存,所有线程对该对象的读写都会第一时间同步到主内存,从而保证多个线程间该对象的准确性

      private volatile static Singleton instance = null;
      // 通过双检锁保证线程安全,只有在instance为null,并创建对象的时候才需要加锁,对性能有一定的提升。
      public static Singleton getInstance(){
      // 先检查实例是否存在,如果不存在才进入下面的同步块
      if(instance == null){
      // 同步块,线程安全的创建实例
      synchronized (Singleton.class) {
      // 再次检查实例是否存在,如果不存在才真正的创建实例
      if(instance == null){
      // 在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,可能导致A线程先赋值给instance,但未完成对象初始化,释放锁后B线程直接调用instance出错。
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }
      // 由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,还有更优的写法吗?
      // 使用静态内部类,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。
      public class Singleton {

      /* 私有构造方法,防止被实例化 */
      private Singleton() {
      }

      /* 此处使用一个内部类来维护单例 */
      private static class SingletonFactory {
      // 当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕
      private static Singleton instance = new Singleton();
      }

      /* 获取实例 */
      public static Singleton getInstance() {
      return SingletonFactory.instance;
      }

      /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
      public Object readResolve() {
      return getInstance();
      }
      }

      // 使用枚举是最简单高效实现单例模式的方案
      public enum Singleton {
      /**
      * 定义一个枚举的元素,它就代表了Singleton的一个实例。
      */
      Instance;
      }
  • 为什么不采用静态方法而是单例模式?

    • 静态方法或单例模式都能实现加载的最终目的,区别在于是否需要维护一份对象。

问:工厂模式是什么?

  • 工厂模式是什么?

    • 一种设计模式,提供了一种将对象创建的过程与其具体实现分离的方法,使得代码具有更好的扩展性和可维护性。
  • 分为哪几类?

    • 简单工厂模式(Simple Factory Pattern):通过一个工厂类,根据传入的参数决定创建哪种具体类的实例。

      • 优点:简单易用,集中管理对象创建逻辑。
      • 缺点:违反开闭原则,增加新的产品时需要修改工厂类代码。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      // 产品接口
      interface Product {
      void use();
      }

      // 具体产品A
      class ProductA implements Product {
      public void use() {
      System.out.println("Using Product A");
      }
      }

      // 具体产品B
      class ProductB implements Product {
      public void use() {
      System.out.println("Using Product B");
      }
      }

      // 工厂类
      class SimpleFactory {
      // 静态工厂模式,每次增加type要修改ifelse
      public static Product createProduct(String type) {
      if (type.equals("A")) {
      return new ProductA();
      } else if (type.equals("B")) {
      return new ProductB();
      }
      throw new IllegalArgumentException("Unknown product type");
      }
      }

      // 使用
      public class Main {
      public static void main(String[] args) {
      Product productA = SimpleFactory.createProduct("A");
      productA.use(); // Output: Using Product A

      Product productB = SimpleFactory.createProduct("B");
      productB.use(); // Output: Using Product B
      }
      }
    • 工厂方法模式(Factory Method Pattern):工厂方法模式将对象的创建延迟到子类中,工厂类则会抽象化,通过子类来决定创建哪种具体类的实例

      • 优点:遵循开闭原则,添加新产品时无需修改现有代码。
      • 缺点:增加了系统的复杂性,需要为每个产品提供一个具体工厂类。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      // 产品接口
      interface Product {
      void use();
      }

      // 具体产品A
      class ProductA implements Product {
      public void use() {
      System.out.println("Using Product A");
      }
      }

      // 具体产品B
      class ProductB implements Product {
      public void use() {
      System.out.println("Using Product B");
      }
      }

      // 抽象工厂接口
      interface Factory {
      Product createProduct();
      }

      // 具体工厂A
      class FactoryA implements Factory {
      public Product createProduct() {
      return new ProductA();
      }
      }

      // 具体工厂B
      class FactoryB implements Factory {
      public Product createProduct() {
      return new ProductB();
      }
      }

      // 使用
      public class Main {
      public static void main(String[] args) {
      Factory factoryA = new FactoryA();
      Product productA = factoryA.createProduct();
      productA.use(); // Output: Using Product A

      Factory factoryB = new FactoryB();
      Product productB = factoryB.createProduct();
      productB.use(); // Output: Using Product B
      }
      }
    • 抽象工程模式(Abstract Factory Pattern):抽象工厂模式提供一个接口,用于创建一系列相关或依赖的对象,而无需指定它们的具体类。工厂方法类中只有一个抽象方法,要想实现多种不同的类对象,只能去创建不同的具体工厂方法的子类来实列化,而抽象工厂 则是让一个工厂负责创建多个不同类型的对象。

      • 优点:分离了具体类的创建,使得代码更加灵活和易于扩展。
      • 缺点:增加了系统的抽象性和复杂性。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      // 产品A接口
      interface ProductA {
      void use();
      }

      // 产品B接口
      interface ProductB {
      void consume();
      }

      // 具体产品A1
      class ProductA1 implements ProductA {
      public void use() {
      System.out.println("Using Product A1");
      }
      }

      // 具体产品A2
      class ProductA2 implements ProductA {
      public void use() {
      System.out.println("Using Product A2");
      }
      }

      // 具体产品B1
      class ProductB1 implements ProductB {
      public void consume() {
      System.out.println("Consuming Product B1");
      }
      }

      // 具体产品B2
      class ProductB2 implements ProductB {
      public void consume() {
      System.out.println("Consuming Product B2");
      }
      }

      // 抽象工厂接口
      interface AbstractFactory {
      ProductA createProductA();
      ProductB createProductB();
      }

      // 具体工厂1
      class Factory1 implements AbstractFactory {
      public ProductA createProductA() {
      return new ProductA1();
      }

      public ProductB createProductB() {
      return new ProductB1();
      }
      }

      // 具体工厂2
      class Factory2 implements AbstractFactory {
      public ProductA createProductA() {
      return new ProductA2();
      }

      public ProductB createProductB() {
      return new ProductB2();
      }
      }

      // 使用
      public class Main {
      public static void main(String[] args) {
      AbstractFactory factory1 = new Factory1();
      ProductA productA1 = factory1.createProductA();
      ProductB productB1 = factory1.createProductB();
      productA1.use(); // Output: Using Product A1
      productB1.consume(); // Output: Consuming Product B1

      AbstractFactory factory2 = new Factory2();
      ProductA productA2 = factory2.createProductA();
      ProductB productB2 = factory2.createProductB();
      productA2.use(); // Output: Using Product A2
      productB2.consume(); // Output: Consuming Product B2
      }
      }
  • 适用场景分别为?

    • 简单工厂模式适用于创建逻辑简单、产品种类较少的场景。
    • 工厂方法模式适用于产品种类繁多,且需要增加新产品时不修改现有代码的场景。当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
    • 抽象工厂模式适用于创建一系列相关或依赖的对象,提供更高的扩展性和灵活性。
  • JDK中的使用案例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    public final class Boolean implements java.io.Serializable,
    Comparable<Boolean>
    {
    // 简单工厂模式
    public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
    }
    }

    // 简单工厂模式
    public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {

    public static Calendar getInstance()
    {
    return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }

    // 懒汉式线程不安全
    private static Calendar createCalendar(TimeZone zone,
    Locale aLocale)
    {
    CalendarProvider provider =
    LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
    .getCalendarProvider();
    // 省略
    Calendar cal = null;

    // 省略
    return cal;
    }
    }

    public class DriverManager {
    public static Connection getConnection(String url,
    java.util.Properties info) throws SQLException {

    return (getConnection(url, info, Reflection.getCallerClass()));
    }

    // 简单工厂模式
    private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {

    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
    // synchronize loading of the correct classloader.
    if (callerCL == null) {
    callerCL = Thread.currentThread().getContextClassLoader();
    }
    }

    for(DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) {
    try {
    Connection con = aDriver.driver.connect(url, info);
    if (con != null) {
    // Success!
    println("getConnection returning " + aDriver.driver.getClass().getName());
    return (con);
    }
    } catch (SQLException ex) {
    if (reason == null) {
    reason = ex;
    }
    }
    } else {
    println(" skipping: " + aDriver.getClass().getName());
    }
    }

    // 省略
    }
    }
  • Spring中的使用案例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76

    // 简单工厂模式:如BeanFactory,根据 Bean 的名称来创建 Bean 实例。
    public interface BeanFactory {
    Object getBean(String name) throws BeansException;
    }

    // 工厂方法模式:如AbstractFactoryBean,提供了未实现的抽象方法createInstance(),将对象创建转移给子类来实现
    // 其中实现FactoryBean接口会创建一个工厂类,getObject方法是Bean的实例化逻辑,返回由工厂创建的对象。
    public abstract class AbstractFactoryBean<T>
    implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {

    @Nullable
    private BeanFactory beanFactory;

    @Override
    public final T getObject() throws Exception {
    if (isSingleton()) {
    return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
    }
    else {
    return createInstance();
    }
    }

    // 此处Spring未实现,方便自定义扩展
    protected abstract T createInstance() throws Exception;
    }

    // 如:ListFactoryBean,通过必需的sourceList,以及可选的targetListClass,将sourceList转换为指定类型的新数组result
    public class ListFactoryBean extends AbstractFactoryBean<List<Object>> {

    private List<?> sourceList;
    private Class<? extends List> targetListClass;

    @Override
    @SuppressWarnings("unchecked")
    protected List<Object> createInstance() {
    if (this.sourceList == null) {
    throw new IllegalArgumentException("'sourceList' is required");
    }
    List<Object> result = null;
    // 此处根据是否指定类型,决定List工厂创建怎样的数组对象,简单工厂模式
    if (this.targetListClass != null) {
    // 指定了类型,通过反射创建数组对象的实例
    result = BeanUtils.instantiateClass(this.targetListClass);
    }
    else {
    // 未指定类型,直接初始化ArrayList
    result = new ArrayList<>(this.sourceList.size());
    }
    Class<?> valueType = null;
    if (this.targetListClass != null) {
    // 指定了类型
    valueType = ResolvableType.forClass(this.targetListClass).asCollection().resolveGeneric();
    }
    if (valueType != null) {
    // 循环转换数组中各个元素的类型
    TypeConverter converter = getBeanTypeConverter();
    for (Object elem : this.sourceList) {
    result.add(converter.convertIfNecessary(elem, valueType));
    }
    }
    else {
    // 未指定类型,简单的复制数组
    result.addAll(this.sourceList);
    }
    return result;
    }
    }

    // 抽象工厂模式:如AbstractBeanFactory实现了如getBean()等的基础逻辑,留了如createBean()等抽象接口扩展,支持不同类型的Bean,由具体的子类工厂来决定产品Bean的生成逻辑,如 DefaultListableBeanFactory 和 XmlBeanFactory
    public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    // 省略
    protected abstract Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
    throws BeanCreationException;
    }

问:生产者消费者模式?要求手写出代码

  • 生产者-消费者模式是一种常见的多线程设计模式,主要解决两个线程之间共享数据的问题。
  • 生产者负责生产数据,消费者负责消费数据。两者通过一个共享的缓冲区进行交互,使用同步机制避免出现资源竞争问题。

通常,生产者-消费者模式涉及以下几个关键点:

  1. 共享资源(缓冲区)。
  2. 使用同步机制(如 wait()notify())来确保数据生产与消费的协调性,避免并发问题。
  3. 阻塞条件:生产者等待缓冲区有空间才能生产,消费者等待缓冲区有数据才能消费。

以下是一个使用 Java 编写的基于 wait()notify() 实现的经典生产者-消费者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 生产者和消费者共享的缓冲区
class SharedQueue {
// 使用 LinkedList 作为内部的队列来存储数据
private final Queue<Integer> queue = new LinkedList<>();
// 容量 capacity 限制了队列的最大大小
private final int capacity;

public SharedQueue(int capacity) {
this.capacity = capacity;
}

// 使用 synchronized 来确保线程安全
// 生产者方法:生产者调用该方法生产数据,如果缓冲区已满,生产者进入等待状态,直到消费者消费了数据。
public synchronized void produce(int value) throws InterruptedException {
// 如果队列已满,等待消费者消费
while (queue.size() == capacity) {
wait();
}
// 生产数据并通知消费者
queue.offer(value);
System.out.println("Produced: " + value);
// 通知所有等待的线程
notifyAll();
}

// 消费者方法:消费者调用该方法消费数据,如果缓冲区为空,消费者进入等待状态,直到生产者生产了数据。
public synchronized int consume() throws InterruptedException {
// 如果队列为空,等待生产者生产
while (queue.isEmpty()) {
wait();
}
// 消费数据并通知生产者
int value = queue.poll();
System.out.println("Consumed: " + value);
// 通知所有等待的线程
notifyAll();
return value;
}
}

class Producer implements Runnable {
private final SharedQueue sharedQueue;

public Producer(SharedQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}

@Override
public void run() {
int value = 0;
try {
while (true) {
sharedQueue.produce(value++);
// 模拟生产过程的延时
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

class Consumer implements Runnable {
private final SharedQueue sharedQueue;

public Consumer(SharedQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}

@Override
public void run() {
try {
while (true) {
sharedQueue.consume();
// 模拟消费过程的延时
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

public class ProducerConsumerTest {
public static void main(String[] args) {
// 创建一个容量为5的共享队列
SharedQueue sharedQueue = new SharedQueue(5);

// 启动生产者线程
Thread producerThread = new Thread(new Producer(sharedQueue));
producerThread.start();

// 启动消费者线程
Thread consumerThread = new Thread(new Consumer(sharedQueue));
consumerThread.start();
}
}

问:观察者模式?

  1. 什么是观察者模式?

    • 观察者模式(Observer Pattern)定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。它广泛用于系统中对象之间的解耦和异步通知。
  2. 模式中有哪些角色?

    • Subject(主题/被观察者):拥有状态,当状态发生变化时通知所有观察者。
    • Observer(观察者):观察被观察者的状态变化并做出响应。
    • ConcreteSubject:具体的主题,持有观察者列表并通知观察者。
    • ConcreteObserver:具体的观察者,实现更新方法,响应主题的状态变化。
  3. 优缺点?

    • 优点:
      • 解耦:观察者与被观察者之间通过接口或抽象类进行通信,被观察者不需要了解观察者的具体实现,降低了耦合度。
      • 扩展性好:可以方便地添加新的观察者,符合开闭原则
      • 异步通信:被观察者状态改变后,通知可以通过事件机制异步触发,适用于实时数据更新的场景。
    • 缺点:
      • 性能问题:如果观察者数量多,通知的开销大,可能导致系统性能下降。
      • 循环依赖:如果两个对象相互依赖且是彼此的观察者,可能会导致循环调用,甚至引发栈溢出。
  4. 使用场景?

    • 当一个对象的改变需要自动通知多个对象时,例如 GUI 中的事件监听机制。
    • 实时系统中,当系统中某个对象发生变化时,希望能通知其他依赖对象。
  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    // 观察者接口
    interface Observer {
    void update(String message);
    }

    // 具体的观察者类
    class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
    this.name = name;
    }

    @Override
    public void update(String message) {
    System.out.println(name + " received message: " + message);
    }
    }

    // 被观察者接口
    interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
    }

    // 具体的被观察者类
    class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String state;

    @Override
    public void registerObserver(Observer observer) {
    observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
    observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
    for (Observer observer : observers) {
    observer.update(state);
    }
    }

    public void setState(String state) {
    this.state = state;
    notifyObservers();
    }
    }

    // 测试代码
    public class ObserverPatternDemo {
    public static void main(String[] args) {
    // 创建主题
    ConcreteSubject subject = new ConcreteSubject();

    // 创建观察者
    Observer observer1 = new ConcreteObserver("Observer 1");
    Observer observer2 = new ConcreteObserver("Observer 2");

    // 注册观察者
    subject.registerObserver(observer1);
    subject.registerObserver(observer2);

    // 修改状态
    subject.setState("State changed to NEW");
    }
    }
  6. JDK中有哪些地方使用了该模式?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    // 观察者接口,定义了 update() 方法
    public interface Observer {
    void update(Observable o, Object arg);
    }

    // 被观察者类,提供了管理观察者的方法如 addObserver() 和 notifyObservers()
    public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;
    public Observable() {
    obs = new Vector<>();
    }

    public synchronized void addObserver(Observer o) {
    if (o == null)
    throw new NullPointerException();
    if (!obs.contains(o)) {
    obs.addElement(o);
    }
    }

    public synchronized void deleteObserver(Observer o) {
    obs.removeElement(o);
    }

    public void notifyObservers() {
    notifyObservers(null);
    }

    public void notifyObservers(Object arg) {
    Object[] arrLocal;

    synchronized (this) {
    if (!changed)
    return;
    arrLocal = obs.toArray();
    clearChanged();
    }

    for (int i = arrLocal.length-1; i>=0; i--)
    ((Observer)arrLocal[i]).update(this, arg);
    }

    // ......
    }

    // 具体的被观察者
    class ConcreteObservable extends Observable {
    private String state;

    public void setState(String state) {
    this.state = state;
    setChanged(); // 标记状态已更改
    notifyObservers(state); // 通知所有观察者
    }
    }

    // 具体的观察者
    class ConcreteObserver implements Observer {
    @Override
    public void update(Observable o, Object arg) {
    System.out.println("Received update: " + arg);
    }
    }

    // 测试
    public class ObserverPatternJDK {
    public static void main(String[] args) {
    ConcreteObservable observable = new ConcreteObservable();
    ConcreteObserver observer = new ConcreteObserver();

    observable.addObserver(observer);
    observable.setState("New State");
    }
    }
  7. Spring有哪些地方使用了该模式?

    Spring 的 ApplicationContext 支持事件发布和监听,这本质上就是观察者模式的实现。

    • **ApplicationEventPublisher**:事件发布者,相当于主题。
    • **ApplicationListener**:事件监听器,相当于观察者。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 观察者
    @FunctionalInterface
    public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
    }

    // 被观察者
    @FunctionalInterface
    public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
    this.publishEvent((Object)event);
    }

    void publishEvent(Object var1);
    }

    // 自定义事件
    public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
    super(source);
    }
    }

    // 事件监听器
    @Component
    public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
    System.out.println("Event received: " + event.getSource());
    }
    }

    // 事件发布
    @Component
    public class EventPublisher {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void publishEvent(String message) {
    publisher.publishEvent(new MyEvent(message));
    }
    }


问:适配器模式?

  1. 什么是适配器模式?

    • 适配器模式(Adapter Pattern)将一个类的接口转换为用户所期望的另一个接口。它的目的是通过适配器类将两个不兼容的接口结合在一起,从而使得原本因接口不兼容而无法一起工作的类能够协同工作。

      适配器模式主要解决的是接口不兼容的问题,常用于复用现有的类库或框架时,避免修改现有的代码,同时使新旧系统兼容。

  2. 模式中有哪些角色?

    • 目标接口(Target Interface):客户端期望使用的接口。
    • 适配器(Adapter):将适配者的接口转换为目标接口。
    • 适配者(Adaptee):需要适配的接口或类。
    • 客户端(Client):使用目标接口的客户代码。
  3. 优缺点?

    • 优点:
      • 提高代码的复用性:通过适配器,可以复用现有的代码,而不需要修改原来的类。
      • 提高系统的灵活性:客户端可以动态地选择合适的适配器,不影响系统的其他部分。
    • 缺点:
      • 复杂度增加:引入适配器类增加了系统的复杂性,尤其是大量使用时。
      • 效率问题:某些情况下适配器模式可能会增加系统的开销,因为增加了一层间接调用。
  4. 使用场景?

    • 当你希望使用一个已经存在的类,但其接口与所需接口不兼容时,可以使用适配器模式。
    • 系统中有多个类接口类似,但不相同,通过适配器模式可以统一接口。
    • 希望将类和类之间的依赖解耦,但现有类接口不符合要求。
  5. 代码实现?

    假设我们有一个VGA接口,现在需要使用HDMI接口的设备,适配器模式可以将HDMI接口转换成VGA接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 目标接口
    interface VGA {
    void showVGA();
    }

    // 适配者类:HDMI
    class HDMI {
    public void showHDMI() {
    System.out.println("Displaying content via HDMI.");
    }
    }

    // 适配器类,将 HDMI 转换为 VGA
    class HDMItoVGAAdapter implements VGA {
    private HDMI hdmi;

    public HDMItoVGAAdapter(HDMI hdmi) {
    this.hdmi = hdmi;
    }

    @Override
    public void showVGA() {
    // 调用 HDMI 的方法
    hdmi.showHDMI();
    }
    }

    // 客户端
    public class AdapterPatternDemo {
    public static void main(String[] args) {
    // 创建适配者对象
    HDMI hdmiDevice = new HDMI();

    // 使用适配器适配 HDMI 到 VGA
    VGA vgaDevice = new HDMItoVGAAdapter(hdmiDevice);
    vgaDevice.showVGA(); // 实际调用的是 HDMI 的 showHDMI()
    }
    }
  6. JDK中有哪些地方使用了该模式?

    • **java.util.Arrays#asList()**:将数组适配为 List
    • **java.io.InputStreamReaderjava.io.OutputStreamWriter**:将字节流适配为字符流。
    • **java.util.Collections#enumeration()java.util.Collections#list()**:将 ListEnumeration 互相转换。
  7. Spring有哪些地方使用了该模式?

    • Spring 的 HandlerAdapter 就是典型的适配器模式,它使得不同类型的控制器(如 @ControllerSimpleController)可以被 DispatcherServlet 处理。如,RequestMappingHandlerAdapter 可以将 @RequestMapping 标注的方法适配为 Handler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190

    package org.springframework.web.reactive;

    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;

    public interface HandlerAdapter {
    boolean supports(Object var1);

    Mono<HandlerResult> handle(ServerWebExchange var1, Object var2);
    }

    // 当 DispatcherServlet 收到一个 HTTP 请求时,它首先会通过不同的 HandlerMapping 实现类来找到与该请求对应的处理器(Handler)。对于 @RequestMapping 注解的方法,RequestMappingHandlerMapping 会负责解析 URL 路径、HTTP 方法等信息,并找到对应的 HandlerMethod。
    // 例如:找到一个标注了 @RequestMapping 的控制器方法
    @Controller
    public class MyController {
    @RequestMapping("/greeting")
    public String greeting() {
    return "Hello, World!";
    }
    }
    // RequestMappingHandlerMapping 将 @RequestMapping 注解解析为一个 HandlerMethod 对象,该对象包含了控制器类的实例(MyController)和方法对象(greeting() 方法)。
    HandlerMethod handlerMethod = new HandlerMethod(controllerInstance, method);

    // 一旦 HandlerMapping 找到合适的 Handler,DispatcherServlet 会将该 Handler 传递给 RequestMappingHandlerAdapter。RequestMappingHandlerAdapter 的任务就是执行这个 HandlerMethod,并返回处理结果。
    public class RequestMappingHandlerAdapter implements HandlerAdapter, ApplicationContextAware, InitializingBean {

    // HTTP 消息读取器的列表,通常用于处理请求体
    private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
    // 用于初始化 Web 数据绑定的对象,比如初始化参数绑定
    @Nullable
    private WebBindingInitializer webBindingInitializer;
    // 处理控制器方法参数解析的配置
    @Nullable
    private ArgumentResolverConfigurer argumentResolverConfigurer;
    // 响应式编程的适配器注册表,用于处理响应式类型的转换
    @Nullable
    private ReactiveAdapterRegistry reactiveAdapterRegistry;
    // 应用上下文,用于获取 Bean 和依赖注入等
    @Nullable
    private ConfigurableApplicationContext applicationContext;
    // 控制器方法的解析器,用于解析请求映射的方法
    @Nullable
    private ControllerMethodResolver methodResolver;
    // 模型初始化器,用于处理请求时的模型数据初始化
    @Nullable
    private ModelInitializer modelInitializer;

    // 省略一些set get方法
    // 设置应用上下文,ApplicationContextAware接口的方法
    public void setApplicationContext(ApplicationContext applicationContext) {
    if (applicationContext instanceof ConfigurableApplicationContext) {
    this.applicationContext = (ConfigurableApplicationContext)applicationContext;
    }

    }

    // 在所有属性设置之后进行初始化,InitializingBean接口的方法
    public void afterPropertiesSet() throws Exception {
    // 确保应用上下文已经设置
    Assert.notNull(this.applicationContext, "ApplicationContext is required");
    // 如果消息读取器为空,则创建默认的读取器
    if (CollectionUtils.isEmpty(this.messageReaders)) {
    ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
    this.messageReaders = codecConfigurer.getReaders();
    }

    // 如果参数解析器配置器为空,创建默认的配置器
    if (this.argumentResolverConfigurer == null) {
    this.argumentResolverConfigurer = new ArgumentResolverConfigurer();
    }

    // 如果响应式适配器注册表为空,使用共享实例
    if (this.reactiveAdapterRegistry == null) {
    this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
    }

    // 初始化方法解析器和模型初始化器
    this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer, this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders);
    this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
    }

    // 判断是否支持给定的 handler,必须是 HandlerMethod 类型
    public boolean supports(Object handler) {
    return handler instanceof HandlerMethod;
    }

    // 处理请求并返回 HandlerResult,核心方法
    public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
    HandlerMethod handlerMethod = (HandlerMethod)handler;
    // 确保 methodResolver 和 modelInitializer 已经初始化
    Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
    // 创建绑定上下文,用于数据绑定和校验
    InitBinderBindingContext bindingContext = new InitBinderBindingContext(this.getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod));
    // 获取可执行的控制器方法
    InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
    // 定义异常处理函数,如果方法抛出异常则使用此函数处理
    Function<Throwable, Mono<HandlerResult>> exceptionHandler = (ex) -> {
    return this.handleException(ex, handlerMethod, bindingContext, exchange);
    };
    // 初始化模型数据并调用控制器方法
    return this.modelInitializer.initModel(handlerMethod, bindingContext, exchange).then(Mono.defer(() -> {
    return invocableMethod.invoke(exchange, bindingContext, new Object[0]);
    })).doOnNext((result) -> {
    result.setExceptionHandler(exceptionHandler);
    }).doOnNext((result) -> {
    bindingContext.saveModel();
    }).onErrorResume(exceptionHandler);
    }

    // 省略异常处理函数
    }

    // 初始化请求处理过程中所需的模型数据
    public class ModelInitializer {

    // 用于解析控制器方法的解析器
    private final ControllerMethodResolver methodResolver;
    // 响应式编程的适配器注册表
    private final ReactiveAdapterRegistry adapterRegistry;
    // 省略构造函数

    // 初始化模型数据,包括处理 @ModelAttribute 注解的方法,并且在需要时从会话中恢复模型数据
    public Mono<Void> initModel(HandlerMethod handlerMethod, InitBinderBindingContext bindingContext, ServerWebExchange exchange) {
    // 获取控制器中的 @ModelAttribute 方法
    List<InvocableHandlerMethod> modelMethods = this.methodResolver.getModelAttributeMethods(handlerMethod);

    // 获取与处理器方法相关联的会话属性处理器
    SessionAttributesHandler sessionAttributesHandler = this.methodResolver.getSessionAttributesHandler(handlerMethod);

    // 如果处理器没有 session attributes,则直接调用 @ModelAttribute 方法来初始化模型
    return !sessionAttributesHandler.hasSessionAttributes() ?
    this.invokeModelAttributeMethods(bindingContext, modelMethods, exchange) :
    // 如果有 session attributes,则从会话中获取数据并将其合并到模型中
    exchange.getSession().flatMap((session) -> {
    Map<String, Object> attributes = sessionAttributesHandler.retrieveAttributes(session);
    bindingContext.getModel().mergeAttributes(attributes);
    bindingContext.setSessionContext(sessionAttributesHandler, session);

    // 执行 @ModelAttribute 方法,并在成功后从 session 中恢复模型属性
    return this.invokeModelAttributeMethods(bindingContext, modelMethods, exchange).doOnSuccess((aVoid) -> {
    this.findModelAttributes(handlerMethod, sessionAttributesHandler).forEach((name) -> {
    if (!bindingContext.getModel().containsAttribute(name)) {
    Object value = session.getRequiredAttribute(name);
    bindingContext.getModel().addAttribute(name, value);
    }
    });
    });
    });
    }

    // 调用所有 @ModelAttribute 方法,初始化模型
    private Mono<Void> invokeModelAttributeMethods(BindingContext bindingContext, List<InvocableHandlerMethod> modelMethods, ServerWebExchange exchange) {
    List<Mono<HandlerResult>> resultList = new ArrayList<>();

    // 遍历每个 @ModelAttribute 方法,并执行它们
    modelMethods.forEach((invocable) -> {
    resultList.add(invocable.invoke(exchange, bindingContext, new Object[0]));
    });

    // 将所有方法的结果汇总并返回
    return Mono.zip(resultList, (objectArray) -> {
    return (List) Arrays.stream(objectArray)
    .map((object) -> this.handleResult((HandlerResult) object, bindingContext))
    .collect(Collectors.toList());
    }).flatMap(Mono::when);
    }

    // 处理 @ModelAttribute 方法的返回值,将其添加到模型中
    private Mono<Void> handleResult(HandlerResult handlerResult, BindingContext bindingContext) {
    Object value = handlerResult.getReturnValue();
    if (value != null) {
    ResolvableType type = handlerResult.getReturnType();
    ReactiveAdapter adapter = this.adapterRegistry.getAdapter(type.resolve(), value);

    // 检查返回值是否是异步类型且为 void
    if (this.isAsyncVoidType(type, adapter)) {
    return Mono.from(adapter.toPublisher(value));
    }

    // 获取属性名并将值放入模型中
    String name = this.getAttributeName(handlerResult.getReturnTypeSource());
    bindingContext.getModel().asMap().putIfAbsent(name, value);
    }

    return Mono.empty();
    }

    // 省略一些方法
    }

问:代理模式?

  1. 什么是代理模式?

    • 代理模式(Proxy Pattern)允许为其他对象提供一个代理,以控制对该对象的访问。代理模式通过为目标对象提供一个代理对象,使得在调用目标对象的方法时,额外的逻辑可以在方法前后进行处理,比如权限控制、日志记录、延迟加载等。
  2. 模式中有哪些角色?

    • Subject(抽象主题):定义代理和目标对象的共同接口,使得客户端可以通过接口与目标对象进行交互。
    • RealSubject(真实主题):实现了 Subject 接口的真实业务类,包含业务逻辑。
    • Proxy(代理类):实现了 Subject 接口,控制对 RealSubject 的访问,可以在调用真实业务方法前后执行一些额外操作。
  3. 优缺点?

    • 优点:
      • 控制访问:代理模式可以控制对目标对象的访问,比如限制某些不符合条件的请求。
      • 增强功能:在不修改原始对象的情况下,可以通过代理类扩展功能,例如在方法执行前后添加日志、监控、事务处理等。
      • 延迟初始化:代理模式可以在目标对象需要时才创建它,节省内存和资源。
    • 缺点:
      • 增加复杂性:引入代理类可能使系统结构变得复杂。
      • 性能开销:代理模式会在调用真实对象的方法时引入额外的调用开销。
  4. 使用场景?

    • 权限控制:通过代理类控制对目标对象的访问权限。
    • 远程代理:为位于不同地址空间的对象提供本地代理,便于本地程序与远程对象通信。
    • 虚拟代理:在目标对象的创建开销很大时,可以通过虚拟代理进行延迟加载。
    • 缓存代理:为目标对象提供缓存功能,避免重复计算。
    • 日志监控:通过代理类在方法调用前后添加日志或性能监控。
  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 1. 抽象主题
    public interface Subject {
    void request();
    }

    // 2. 真实主题
    public class RealSubject implements Subject {
    @Override
    public void request() {
    System.out.println("RealSubject: Handling request.");
    }
    }

    // 3. 代理类
    public class Proxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
    if (realSubject == null) {
    realSubject = new RealSubject();
    }
    System.out.println("Proxy: Logging before request.");
    realSubject.request();
    System.out.println("Proxy: Logging after request.");
    }
    }

    // 4. 客户端代码
    public class Client {
    public static void main(String[] args) {
    Subject proxy = new Proxy();
    proxy.request(); // 使用代理对象来调用
    }
    }

    // 输出:
    Proxy: Logging before request.
    RealSubject: Handling request.
    Proxy: Logging after request.
  6. JDK中有哪些地方使用了该模式?JDK 提供的动态代理可以在运行时动态生成代理类,而不需要手动定义代理类。JDK 动态代理通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口实现,是 Java 内置的代理模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 1. 抽象主题
    public interface Subject {
    void request();
    }

    // 2. 真实主题
    public class RealSubject implements Subject {
    @Override
    public void request() {
    System.out.println("RealSubject: Handling request.");
    }
    }

    // 3. 动态代理类
    public class DynamicProxyHandler implements InvocationHandler {
    private Object realSubject;

    public DynamicProxyHandler(Object realSubject) {
    this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("Proxy: Logging before request.");
    Object result = method.invoke(realSubject, args);
    System.out.println("Proxy: Logging after request.");
    return result;
    }
    }

    // 4. 客户端代码
    public class Client {
    public static void main(String[] args) {
    RealSubject realSubject = new RealSubject();
    Subject proxyInstance = (Subject) Proxy.newProxyInstance(
    realSubject.getClass().getClassLoader(),
    realSubject.getClass().getInterfaces(),
    new DynamicProxyHandler(realSubject)
    );
    proxyInstance.request(); // 使用动态代理调用方法
    }
    }

  7. Spring有哪些地方使用了该模式?Spring AOP(面向切面编程)使用了代理模式。使用 @Transactional 或其他 AOP 注解时,Spring 会通过代理模式为目标方法添加事务管理。

    • JDK 动态代理:使用 Proxy.newProxyInstance 动态生成代理对象。
    • CGLIB 代理:通过生成目标类的子类来实现代理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
       
    public static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces,
    InvocationHandler h)
    throws IllegalArgumentException
    {
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
    checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
    * Look up or generate the designated proxy class.
    */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
    * Invoke its constructor with the designated invocation handler.
    */
    try {
    if (sm != null) {
    checkNewProxyPermission(Reflection.getCallerClass(), cl);
    }

    final Constructor<?> cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    if (!Modifier.isPublic(cl.getModifiers())) {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
    cons.setAccessible(true);
    return null;
    }
    });
    }
    return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
    throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
    Throwable t = e.getCause();
    if (t instanceof RuntimeException) {
    throw (RuntimeException) t;
    } else {
    throw new InternalError(t.toString(), t);
    }
    } catch (NoSuchMethodException e) {
    throw new InternalError(e.toString(), e);
    }
    }

问:模板方法模式?

  1. 什么是模板方法模式?

    • 模板方法模式(Template Method Pattern)定义了一个操作的算法框架,而将某些步骤的实现延迟到子类。通过模板方法模式,子类可以在不改变算法整体结构的情况下,重新定义某些特定步骤的实现。
  2. 模式中有哪些角色?

    • AbstractClass(抽象类):定义了算法的框架,包含一个模板方法 templateMethod() 和一些抽象方法,具体的步骤由子类实现。
    • ConcreteClass(具体类):实现了 AbstractClass 的抽象方法,完成算法的各个步骤。
  3. 优缺点?

    • 优点:
      • 复用代码:将算法的公共部分放在父类,减少了代码重复。
      • 灵活扩展:子类可以通过实现不同的具体步骤来扩展算法,保持了代码的灵活性。
      • 控制流程:父类通过定义模板方法控制整体流程,保证框架的稳定性。
    • 缺点:
      • 增加复杂性:引入了抽象类和子类,可能导致类的数量增加,增加了系统的复杂性。
      • 扩展困难:如果新增的步骤需要改变模板方法的结构,则可能需要修改父类,这违反了开闭原则。
  4. 使用场景?

    • 固定流程:某些算法流程是固定的,但其中某些步骤是可变的,可以通过子类实现不同的细节。
    • 代码复用:当多个类中存在相同的逻辑,但部分步骤不同,可以通过模板方法模式复用代码,减少重复。
    • 框架设计:在框架设计中,模板方法模式可以定义流程,用户通过继承并实现具体步骤来扩展功能。
  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // 1. 抽象类,定义算法框架
    public abstract class Game {
    // 模板方法,定义了游戏的流程
    public final void play() {
    start();
    playGame();
    end();
    }

    // 固定步骤
    public void start() {
    System.out.println("Game started.");
    }

    // 抽象方法,子类实现
    protected abstract void playGame();

    // 固定步骤
    public void end() {
    System.out.println("Game ended.");
    }
    }

    // 2. 具体类,实现不同的步骤
    public class Football extends Game {
    @Override
    protected void playGame() {
    System.out.println("Playing Football.");
    }
    }

    public class Basketball extends Game {
    @Override
    protected void playGame() {
    System.out.println("Playing Basketball.");
    }
    }

    // 3. 客户端代码
    public class Client {
    public static void main(String[] args) {
    Game football = new Football();
    football.play(); // 按照模板方法执行游戏流程

    Game basketball = new Basketball();
    basketball.play();
    }
    }
    // 输出:
    Game started.
    Playing Football.
    Game ended.
    Game started.
    Playing Basketball.
    Game ended.
  6. JDK中有哪些地方使用了该模式?

    • java.util.AbstractList 和 **java.util.AbstractSet**:这些类使用了模板方法模式,在其中定义了某些通用的集合操作,而子类只需实现少量的具体方法。例如,在 AbstractList 中,get() 是抽象的,由子类实现,而 add()remove() 等方法则是固定的。
    • **java.io.InputStreamjava.io.OutputStream**:这些流类中,read()write() 是模板方法的一种实现,具体的读取和写入操作由子类实现。
  7. Spring有哪些地方使用了该模式?Spring 提供了一些模板类,如 **JdbcTemplateRestTemplate**,这些类封装了常见操作的流程,开发者只需定义具体的回调操作

    JdbcTemplate 和其他类似的模板类:Spring 提供了 JdbcTemplateRestTemplateJmsTemplate 等类,这些类通过模板方法封装了操作的通用流程。开发者只需提供特定的操作实现,如 SQL 查询、HTTP 请求等。例如,在 JdbcTemplate 中,SQL 执行的流程是固定的,开发者只需要通过回调函数提供具体的 SQL 执行逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    jdbcTemplate.query("SELECT * FROM users", new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    User user = new User();
    user.setId(rs.getInt("id"));
    user.setName(rs.getString("name"));
    return user;
    }
    });

    **AbstractApplicationContext**:在 Spring 的 AbstractApplicationContext 中,refresh() 方法是一个典型的模板方法,定义了启动 Spring 容器的整个流程。子类可以通过扩展某些步骤来实现不同的初始化逻辑。

    **AbstractPlatformTransactionManager**:Spring 中的事务管理器类使用了模板方法模式,定义了事务的基本处理流程,具体的事务处理逻辑由子类实现,如 DataSourceTransactionManagerJpaTransactionManager

问:策略模式?

  1. 什么是策略模式?

    • 策略模式(Strategy Pattern)定义了一系列算法,并将每种算法封装在独立的类中,使它们可以互相替换。策略模式允许客户端动态地选择不同的算法,避免使用条件语句来选择算法,从而实现算法的独立扩展和替换。
  2. 模式中有哪些角色?

    • Context(上下文):负责与客户端交互,并维护一个对策略对象的引用。
    • Strategy(策略接口):定义算法的通用接口。
    • ConcreteStrategy(具体策略类):实现具体的算法。
  3. 优缺点?

    • 优点:
      • 算法的独立性:不同的算法封装在独立的类中,易于理解和维护。
      • 扩展性好:增加新的算法时,只需要添加新的策略类,而不影响已有的代码。
      • 避免条件判断:通过使用策略类来代替多重条件或 if-else 语句,代码结构更加清晰。
    • 缺点:
      • 类的数量增加:每个策略都需要定义为一个类,类的数量会增加。
      • 客户端必须了解所有策略:客户端必须知道有哪些可用的策略,并选择合适的策略。
  4. 使用场景?

    • 多个算法可以互相替换:例如,在支付系统中,用户可以选择不同的支付方式(如微信、支付宝、信用卡等),这些支付方式都是策略模式的具体实现。
    • 避免多个条件判断:在某些情况下,系统中存在大量的条件语句,选择不同的算法时可以使用策略模式。
    • 需要动态地选择算法:策略模式允许在运行时根据条件动态地选择不同的算法。
  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    // 1. 策略接口
    public interface PaymentStrategy {
    void pay(int amount);
    }

    // 2. 具体策略类
    public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
    this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
    System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
    }
    }

    public class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
    this.email = email;
    }

    @Override
    public void pay(int amount) {
    System.out.println("Paid " + amount + " using PayPal: " + email);
    }
    }

    // 3. Context 类
    public class PaymentContext {
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
    this.strategy = strategy;
    }

    public void setPaymentStrategy(PaymentStrategy strategy) {
    this.strategy = strategy;
    }

    public void payAmount(int amount) {
    strategy.pay(amount);
    }
    }

    // 4. 客户端代码
    public class Client {
    public static void main(String[] args) {
    PaymentContext context = new PaymentContext(new CreditCardPayment("1234-5678-9012-3456"));
    context.payAmount(100); // 使用信用卡支付

    context.setPaymentStrategy(new PayPalPayment("user@example.com"));
    context.payAmount(200); // 使用 PayPal 支付
    }
    }

    // 输出:
    Paid 100 using Credit Card: 1234-5678-9012-3456
    Paid 200 using PayPal: user@example.com
  6. JDK中有哪些地方使用了该模式?

    **java.util.Comparator**:Comparator 接口就是策略模式的一个典型例子。它允许用户定义不同的排序策略,并且可以在 Collections.sort 等方法中使用不同的 Comparator 实现。

    1
    2
    3
    Comparator<String> byLength = (s1, s2) -> Integer.compare(s1.length(), s2.length());
    List<String> list = Arrays.asList("apple", "banana", "cherry");
    Collections.sort(list, byLength);

    **javax.servlet.http.HttpServlet**:HttpServletdoGetdoPost 等方法实现了不同的 HTTP 请求方式处理策略。

  7. Spring有哪些地方使用了该模式?

    • **SpringConversionService**:Spring 的类型转换服务是策略模式的典型应用。ConversionService 定义了通用的接口,可以使用不同的策略来执行类型转换。Spring 提供了不同的 Converter 实现,用于在不同类型之间进行转换,而开发者也可以自己定义新的 Converter
    • Spring 的事务管理:Spring 的事务管理使用策略模式定义了不同的事务传播行为(如 REQUIREDREQUIRES_NEWSUPPORTS 等),通过 @Transactional 注解或编程方式,用户可以选择合适的事务策略。**TransactionManagement**:Spring 的事务管理器允许配置不同的事务策略,如编程式事务和声明式事务。这也是策略模式的应用,通过设置不同的事务策略,用户可以灵活选择事务的管理方式。
    • BeanFactory 的后置处理器:当 Spring 容器初始化时,BeanFactoryPostProcessorBeanPostProcessor 可以定义不同的处理策略,来在 Bean 创建的不同生命周期中定制处理逻辑。Spring 的 IOC 容器中使用了策略模式来决定如何创建和管理 Beans。BeanFactory 允许通过不同的策略(例如单例、原型模式)来管理 Bean 的生命周期。
    • Resource 接口Resource 是 Spring 用来抽象文件访问的接口,具体的实现包括 ClassPathResourceFileSystemResource 等。这些不同的资源访问方式可以通过策略模式轻松替换。
    • Cache 策略:Spring 的缓存抽象层也采用了策略模式,支持使用不同的缓存实现(如 EhCacheRedisGuava),用户可以根据需求选择不同的缓存策略。

问:责任链模式?

  1. 什么是责任链模式?

    • 责任链模式(Chain of Responsibility Pattern)允许多个对象依次处理请求,直到其中一个对象处理该请求为止。通过这种方式,请求发送者无需指定处理者,处理者可以动态组合,灵活改变处理流程。
  2. 模式中有哪些角色?

    • Handler(抽象处理者):定义处理请求的接口,包含处理请求和将请求传递给下一个处理者的逻辑。
    • ConcreteHandler(具体处理者):具体处理请求的类,继承 Handler。如果自己能处理请求就处理,否则将请求传递给下一个处理者。
    • Client(客户端):向责任链的第一个处理者提交请求,不需要关心具体由哪个处理者处理。
  3. 优缺点?

    • 优点:
      • 解耦请求发送者和处理者:请求发送者不需要知道请求是由哪个处理者处理的,处理者之间的逻辑变得清晰。
      • 灵活性高:可以灵活地组合和修改责任链中的处理者。
      • 可扩展性强:通过添加新的处理者,可以扩展责任链而无需修改已有代码。
    • 缺点:
      • 性能问题:如果链条过长,处理请求时可能会导致性能问题,因为每个请求都需要遍历链条上的每个处理者。
      • 调试困难:由于请求是沿着链条传递的,可能很难确定是哪一个处理者处理了请求。
  4. 使用场景?

    • 多个对象可以处理同一请求:但只有一个对象处理该请求时使用责任链模式。
    • 动态指定处理者:当你希望在运行时动态指定处理者的顺序时,可以使用责任链模式。
    • 需要对请求进行预处理和后处理:如在表单验证、权限校验、日志记录等场景中,责任链模式比较适用。
  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // 1. 抽象处理者
    abstract class Handler {
    protected Handler nextHandler;

    public void setNextHandler(Handler nextHandler) {
    this.nextHandler = nextHandler;
    }

    public abstract void handleRequest(String request);
    }

    // 2. 具体处理者
    class ConcreteHandlerA extends Handler {
    @Override
    public void handleRequest(String request) {
    if (request.equals("A")) {
    System.out.println("ConcreteHandlerA handled the request.");
    } else if (nextHandler != null) {
    nextHandler.handleRequest(request);
    }
    }
    }

    class ConcreteHandlerB extends Handler {
    @Override
    public void handleRequest(String request) {
    if (request.equals("B")) {
    System.out.println("ConcreteHandlerB handled the request.");
    } else if (nextHandler != null) {
    nextHandler.handleRequest(request);
    }
    }
    }

    // 3. 客户端代码
    public class Client {
    public static void main(String[] args) {
    // 创建处理者链
    Handler handlerA = new ConcreteHandlerA();
    Handler handlerB = new ConcreteHandlerB();
    handlerA.setNextHandler(handlerB);

    // 提交请求
    handlerA.handleRequest("A"); // ConcreteHandlerA handled the request.
    handlerA.handleRequest("B"); // ConcreteHandlerB handled the request.
    handlerA.handleRequest("C"); // 没有处理者处理该请求
    }
    }

    // 输出:
    ConcreteHandlerA handled the request.
    ConcreteHandlerB handled the request.
  6. JDK中有哪些地方使用了该模式?

    **java.util.logging.Logger**:Java 的日志框架中使用了责任链模式。Logger 可以将日志记录请求传递给父 Logger,直到找到可以处理该请求的 Logger

    1
    2
    3
    4
    javaCopy codeLogger logger = Logger.getLogger("com.example");
    logger.setLevel(Level.INFO);
    Logger parentLogger = Logger.getLogger("com");
    parentLogger.setLevel(Level.WARNING);

    **javax.servlet.Filter**:Filter 是 Servlet 规范中的一个接口,用于对 HTTP 请求进行预处理或后处理。Filter 形成一个责任链,依次处理请求,处理完一个 Filter 后请求会被传递给下一个 Filter

  7. Spring有哪些地方使用了该模式?

    **HandlerInterceptor**:Spring MVC 中的 HandlerInterceptor 也是一个责任链模式的实现。多个拦截器可以依次对请求进行处理,类似于 Servlet 的过滤器链。

    1
    2
    3
    4
    5
    6
    7
    public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    // 在处理请求之前进行拦截
    return true; // 返回 true 表示继续往下传递请求
    }
    }

    **Filter**:Spring 的 Web 层基于 Servlet 规范,也使用了过滤器链来处理 HTTP 请求。例如,Spring Security 的过滤器链就应用了责任链模式,处理认证、授权、访问控制等。

    **TransactionManager**:Spring 的事务管理机制也采用了责任链模式。Spring 支持多种事务管理方式(如 DataSourceTransactionManagerJpaTransactionManager 等),每种事务管理器都可以处理不同的事务,并且可以动态地选择最合适的事务管理器。

问:装饰者模式?

  1. 什么是装饰者模式?

    • 装饰者模式(Decorator Pattern)允许动态地给对象添加新的功能,而不会影响其他同类对象的功能。装饰者模式通过将功能分层次地添加给对象,比继承更灵活,也更符合开闭原则(对扩展开放,对修改封闭)。
  2. 模式中有哪些角色?

    • Component(抽象组件):定义对象的接口,可以被动态添加职责。

      ConcreteComponent(具体组件):被装饰的原始对象,装饰者通过此类进行功能扩展。

      Decorator(抽象装饰者):持有组件对象的引用,并且实现与 Component 相同的接口,目的是对组件对象进行装饰。

      ConcreteDecorator(具体装饰者):通过继承装饰者类,向组件添加职责(即扩展功能)。

  3. 优缺点?

    • 优点:
      • 扩展性强:可以动态地给对象增加新的功能,不需要修改原始类的代码。

        灵活组合:通过使用不同的装饰者,能自由组合对象的功能。

        遵循开闭原则:不修改已有的代码,通过装饰者类来扩展对象的功能。

    • 缺点:
      • 增加复杂性:使用装饰者模式会导致系统中类的数量增加,且增加层次结构,调试困难。

        比继承开销大:由于使用组合和多次包装,可能在性能上比继承略差。

  4. 使用场景?

    • 需要在不修改现有代码的情况下扩展类的功能

      需要动态地为对象添加功能,且可以随时撤销这些功能

      需要通过不同的组合方式,实现功能的叠加,例如输入输出流的处理。

  5. 代码实现?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    // 1. 抽象组件
    interface Component {
    void operation();
    }

    // 2. 具体组件
    class ConcreteComponent implements Component {
    @Override
    public void operation() {
    System.out.println("ConcreteComponent: 基本操作");
    }
    }

    // 3. 抽象装饰者
    abstract class Decorator implements Component {
    protected Component component;

    public Decorator(Component component) {
    this.component = component;
    }

    @Override
    public void operation() {
    component.operation();
    }
    }

    // 4. 具体装饰者 A
    class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
    super(component);
    }

    @Override
    public void operation() {
    super.operation();
    System.out.println("ConcreteDecoratorA: 添加额外功能 A");
    }
    }

    // 5. 具体装饰者 B
    class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
    super(component);
    }

    @Override
    public void operation() {
    super.operation();
    System.out.println("ConcreteDecoratorB: 添加额外功能 B");
    }
    }

    // 6. 客户端代码
    public class Client {
    public static void main(String[] args) {
    // 原始对象
    Component component = new ConcreteComponent();

    // 装饰对象 A
    Component decoratedA = new ConcreteDecoratorA(component);
    decoratedA.operation();
    // Output: ConcreteComponent: 基本操作
    // ConcreteDecoratorA: 添加额外功能 A

    // 装饰对象 A 和 B
    Component decoratedB = new ConcreteDecoratorB(decoratedA);
    decoratedB.operation();
    // Output: ConcreteComponent: 基本操作
    // ConcreteDecoratorA: 添加额外功能 A
    // ConcreteDecoratorB: 添加额外功能 B
    }
    }
  6. JDK中有哪些地方使用了该模式?

    **java.io.InputStreamOutputStreamReaderWriter**:Java IO 库中大量使用了装饰者模式。以 BufferedInputStream 为例,它扩展了 InputStream 的功能,通过缓冲机制提高了读写效率,但它依然是 InputStream 的一个子类,保持原有的接口。

    1
    2
    InputStream inputStream = new FileInputStream("file.txt");
    InputStream buffered = new BufferedInputStream(inputStream);

    **Collections.unmodifiableListCollections.synchronizedList**:这些方法通过装饰者模式为原始 List 添加了线程安全或不可修改的功能。

    1
    2
    List<String> list = new ArrayList<>();
    List<String> synchronizedList = Collections.synchronizedList(list);
  7. Spring有哪些地方使用了该模式?

    **Transaction Management**:Spring 的事务管理机制在方法级别上实现了装饰者模式。Spring 的 TransactionProxy 为方法添加了事务功能,而不需要改变原有方法的实现。

    1
    2
    3
    4
    @Transactional
    public void doSomething() {
    // 方法被装饰为带事务支持
    }

    **BeanPostProcessor**:Spring 中的 BeanPostProcessor 可以在初始化之前或之后对 Bean 进行装饰。BeanPostProcessor 是对 Bean 的装饰器,可以在 Spring 容器初始化 Bean 时,动态地为其添加行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class CustomBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("Before Initialization: " + beanName);
    return bean; // 可以在这里修改 bean
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("After Initialization: " + beanName);
    return bean; // 可以在这里修改 bean
    }
    }

    HttpServletRequestWrapper 和 **HttpServletResponseWrapper**:Spring Web 中对 HTTP 请求和响应对象进行了装饰。通过这些装饰类,开发者可以在不修改原有 HttpServletRequestHttpServletResponse 对象的基础上添加自定义功能。

    1
    2
    3
    4
    5
    6
    HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request) {
    @Override
    public String getParameter(String name) {
    return super.getParameter(name).toUpperCase();
    }
    };

问:设计模式中常见原则知道一些吗?面向对象设计的五大基本原则?

  • 什么是面向对象设计的五大基本原则?简称SOLID原则

    1. 单一职责原则(Single Responsibility Principle,SRP)

      定义:一个类应该只有一个引起它变化的原因,换句话说,一个类只应该有一个职责

      目的:将不同的职责分离到不同的类中,避免一个类承担过多的责任,从而提高类的可维护性和可读性。如果一个类有多个职责,那么其中一个职责的变更可能会影响到其他职责的功能

      示例:可以将验证用户的功能拆分到另一个类中,从而遵循单一职责原则。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class UserService {
      public void addUser() {
      // 负责添加用户
      }

      public void validateUser() {
      // 负责验证用户
      }
      }
    2. 开闭原则(Open/Closed Principle,OCP)

      定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,Open for extension,Closed for modification。

      目的:提高系统的可扩展性,降低由于代码修改而引入的风险。通过扩展(比如继承、实现接口等)来增加新功能,而不是通过修改已有代码。从而保证可维护、可扩展、可复用和高灵活性。开发人员应该对程序中呈现出频繁变化的部分做出抽象。在实际开发中,将抽象层和实现层分离,不修改抽象,只处理实现。

      示例:通过新增Shape的实现类可以扩展功能,而不需要修改已有的Shape接口和实现类。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      interface Shape {
      void draw();
      }

      class Circle implements Shape {
      public void draw() {
      // 绘制圆形
      }
      }

      class Rectangle implements Shape {
      public void draw() {
      // 绘制矩形
      }
      }
    3. 里氏替换原则(Liskov Substitution Principle,LSP)

      定义子类必须能够替换其基类,且子类替换基类后,程序的行为不会改变。

      目的:保证继承的正确性,避免继承关系中子类对基类的修改导致系统功能的异常或崩溃。

      示例:这里鸵鸟(Ostrich)类违背了里氏替换原则,因为它不能替代鸟(Bird)类。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Bird {
      public void fly() {
      // 鸟会飞
      }
      }

      class Ostrich extends Bird {
      @Override
      public void fly() {
      throw new UnsupportedOperationException("鸵鸟不会飞");
      }
      }
    4. 接口隔离原则(Interface Segregation Principle,ISP)

      定义:客户端不应该被迫依赖它不使用的方法,接口应该尽量细化,并为不同的类提供特定的接口。

      目的:避免接口臃肿,客户端只需要关心它需要的方法,减少不必要的依赖。

      示例:这里接口Printer不符合接口隔离原则,可以将其拆分为更小的接口。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      interface Printer {
      void print();
      void scan();
      void fax();
      }

      class SimplePrinter implements Printer {
      public void print() {
      // 打印功能
      }

      public void scan() {
      // 没有扫描功能,但必须实现这个方法
      }

      public void fax() {
      // 没有传真功能,但必须实现这个方法
      }
      }
    5. 依赖倒置原则(Dependency Inversion Principle,DIP)

      定义高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。

      目的:减少类之间的耦合,使得系统更容易维护和扩展。通过依赖于抽象(接口或抽象类)而非具体实现,可以提高系统的灵活性。

      示例Switch类依赖于具体的Light类,可以通过引入Light接口来遵循依赖倒置原则。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Light {
      public void turnOn() {
      // 打开灯
      }
      }

      class Switch {
      private Light light;

      public Switch(Light light) {
      this.light = light;
      }

      public void operate() {
      light.turnOn();
      }
      }
  • 迪米特法则也叫最少知识原则(Principle of Least Knowledge),一个类应当对其它类尽可能少去了解。其实就是类尽量不把与别的类的交互放在内部函数中,尽量放在第三方类处理;类尽量将成员变量和方法设为私有。从而降低类之间耦合,提高复用。

    • 迪米特法则规定:

      • 一个对象应该只与直接相关的对象通信,不要与陌生对象发生直接联系。
      • 尽量避免通过一个对象去访问另一个对象的内部细节(通过链式调用等)。
    • 在迪米特法则中,朋友对象是指:

      1. 当前对象本身。
      2. 当前对象的成员变量。
      3. 当前对象创建的对象。
      4. 当前对象的方法参数。
    • 为什么使用迪米特法则?

      • 迪米特法则的主要目的是降低系统的耦合性,提高系统的可维护性可扩展性。通过减少对象之间的直接依赖,可以减少由于对象的内部变化而导致的系统变更的范围。
    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      class Engine {
      public void start() {
      System.out.println("Engine started");
      }
      }

      class Car {
      private Engine engine;

      public Car() {
      this.engine = new Engine();
      }

      public Engine getEngine() {
      return engine;
      }
      }

      class Driver {
      public void drive(Car car) {
      car.getEngine().start(); // 违反迪米特法则,Driver直接访问了Car的Engine
      }
      }

      在这个例子中,Driver类直接访问了CarEngine对象,这违反了迪米特法则。如果将来Car类的内部实现发生了变化,比如Engine的结构发生改变,那么Driver类也可能需要相应修改。

      可以通过调整代码来遵循迪米特法则:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Car {
      private Engine engine;

      public Car() {
      this.engine = new Engine();
      }

      public void startEngine() {
      engine.start(); // 提供一个方法来启动引擎
      }
      }

      class Driver {
      public void drive(Car car) {
      car.startEngine(); // 遵循迪米特法则,Driver只与Car通信
      }
      }

      在这个改进后的例子中,Driver类只与Car类通信,而不直接访问Car的内部细节(如Engine对象),从而遵循了迪米特法则。

    • 迪米特法则有助于减少类之间的耦合,使得系统的维护和扩展变得更加容易。它提醒开发者要尽量避免对象之间复杂的依赖关系,通过降低对象之间的直接关联来提高代码的健壮性和可维护性。

九. Java 8 新特性

问:java8有了解吗?

  1. Lambda表达式:提供了一种清晰简洁的方式来表示单方法接口(函数式接口)的实现。该特性使得 Java 更加适合函数式编程。

    1
    2
    List<String> names = Arrays.asList("John", "Jane", "Max");
    names.forEach(name -> System.out.println(name));
  2. 流 - Streams API:Streams 允许你以声明式的方式处理数据集合,使代码更具可读性和可维护性。它支持诸如 filter(过滤)、map(映射)、reduce(归约)等操作。

    1
    2
    3
    4
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
  3. 接口中的默认方法:Java 8 引入了默认方法,允许接口具有方法的实现。这样可以在不破坏现有代码的情况下更新 API。

    示例:

    1
    2
    3
    4
    5
    interface Vehicle {
    default void honk() {
    System.out.println("Honking");
    }
    }
  4. Optional 类Optional 类用于避免 null 检查,防止 NullPointerException。它提供了一些方法来处理可能为空的值。

    示例:

    1
    2
    Optional<String> name = Optional.ofNullable(null);
    System.out.println(name.orElse("Default Name"));
  5. 新的日期和时间 API(java.time 包):Java 8 引入了一个更加直观且强大的日期和时间处理 API,解决了 java.util.Datejava.util.Calendar 的不足。

    示例:

    1
    2
    LocalDate date = LocalDate.now();
    LocalTime time = LocalTime.now();
  6. 函数式接口:Java 8 引入了若干内置的函数式接口,如 PredicateFunctionSupplierConsumer

    示例:

    1
    2
    Predicate<Integer> isPositive = x -> x > 0;
    System.out.println(isPositive.test(5)); // 输出: true

Java8的新特性

问:讲讲Lambda表达式?使用场景?优点?

  1. Lambda 表达式是 Java 8 引入的一种简洁的函数式编程方式,允许你将行为(函数或方法)作为参数传递。Lambda 表达式通常用于替代匿名类,解决了Java无法传递代码段的问题,并使代码更加简洁、可读。它的语法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    (parameters) -> expression
    或者
    (parameters) -> { statements; }

    // 示例:无参数
    () -> System.out.println("Hello, Lambda!");

    // 示例:有参数
    (int x, int y) -> x + y;

    // 示例:带代码块
    (int x, int y) -> {
    int sum = x + y;
    return sum;
    };
  2. 使用场景:

    1. 替代匿名类: Lambda 表达式可以用于简化实现单方法接口的匿名类,尤其是函数式接口的实现。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 传统方式:匿名类
      Runnable runnable = new Runnable() {
      @Override
      public void run() {
      System.out.println("Running");
      }
      };

      // Lambda 表达式
      Runnable runnableLambda = () -> System.out.println("Running");
    2. 集合操作: 在使用集合类时,Lambda 表达式通常结合 Stream API 来进行过滤、排序和转换操作。

      1
      2
      3
      4
      5
      6
      List<String> names = Arrays.asList("John", "Jane", "Mike");

      // 使用 Lambda 表达式筛选名字长度大于 3 的元素
      List<String> filteredNames = names.stream()
      .filter(name -> name.length() > 3)
      .collect(Collectors.toList());
    3. 事件处理: 在 GUI 编程或其他基于事件的编程中,Lambda 表达式可以简化事件处理代码。例如,在 JavaFX 中设置按钮的点击事件:

      1
      2
      Button button = new Button("Click me");
      button.setOnAction(e -> System.out.println("Button clicked!"));
    4. 多线程编程: Lambda 表达式可以用于简化多线程编程中的 RunnableCallable 的实现:

      1
      new Thread(() -> System.out.println("Running in a thread")).start();
  3. 优点:

    1. 简洁易读: Lambda 表达式消除了创建匿名类时的冗长代码,使代码更加简洁,减少了样板代码(如方法签名、类声明等)。
    2. 更好的抽象能力: Lambda 表达式可以作为参数传递,允许程序更加灵活地定义和传递行为,促进代码重用和模块化设计。
    3. 高效并行流处理: 结合 Stream API,Lambda 表达式能够简洁地表达集合操作,并可以轻松并行化数据处理任务。
    4. 提高生产力: 简化了开发过程中的重复性工作,开发者可以专注于业务逻辑而不是编写样板代码。
  4. 代码示例:

    使用 Lambda 表达式处理集合

    1
    2
    3
    4
    5
    6
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

    // 过滤名字长度大于3的元素
    names.stream()
    .filter(name -> name.length() > 3)
    .forEach(System.out::println);

    使用 Lambda 表达式创建线程

    1
    new Thread(() -> System.out.println("Thread running")).start();

问:谈谈流?和集合对比?

Stream 是 Java 8 中引入的一种用于处理集合数据的 API,允许对集合中的元素进行链式的操作,如过滤、排序、转换、聚合等。Stream 提供了一种声明式编程的方式来处理数据,简化了对集合的操作,同时支持并行处理,提高了性能。

核心特性:

  • 不存储数据Stream 本身不存储数据,而是从源(如集合、数组等)获取数据。
  • 惰性求值Stream 的中间操作是惰性的,只有在终端操作时才会实际执行。
  • 函数式编程Stream 提供了多个用于操作元素的函数式方法,如 filtermapreduce 等。
  • 可并行处理:通过 parallelStream() 可以轻松实现并行处理。

使用场景对比:

  • 集合:适合需要频繁增删改查的数据操作场景,尤其是需要多次访问或操作集合中的元素时。
  • :适合对数据进行过滤、转换、聚合等操作,特别是数据处理链较复杂,且需要高性能并行处理时。

流是为了在一些如需要进行复杂计算以及多核的场景下代替集合而设计的。集合的主要目的是为了存储和访问数据,而流则是主要用于描述对数据的计算。相较于集合,流采用了更高级的语言表达,流水线的结构可以减少遍历,自动的并行化流操作可以有效利用多核性能。

流的优点:

  1. 简洁性:流允许使用链式操作来处理集合中的元素,减少了样板代码,提升了可读性。
  2. 函数式编程:流提供了对集合进行过滤、转换、聚合的函数式编程接口,支持声明式编程。
  3. 并行处理:流的并行处理能力使得在多核环境下更容易提高性能。

集合的优点

  1. 存储功能:集合类提供了持久的元素存储功能,可以随时添加或删除元素。
  2. 可重复操作:集合可以多次遍历和修改,支持复杂的数据结构和操作。

问:default关键字 ?

default 关键字在 Java 8 中被引入,用于为接口中的方法提供默认实现。它主要用于解决接口演变问题,使得在现有接口中添加新方法时,不必修改所有实现该接口的类。打破接口里面是只能有抽象方法,不能有任何方法的实现,接口里面也可以有方法的实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface MyInterface {
// 默认方法
default void sayHello() {
System.out.println("Hello from MyInterface");
}

// 抽象方法
void abstractMethod();
}

class MyClass implements MyInterface {
@Override
public void abstractMethod() {
System.out.println("Implementing abstract method");
}

// 可以选择重写默认方法
@Override
public void sayHello() {
System.out.println("Hello from MyClass");
}
}

public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.sayHello(); // 输出:Hello from MyClass
myClass.abstractMethod(); // 输出:Implementing abstract method
}
}

问:新时间日期 APILocalDate | LocalTime | LocalDateTime ?对比旧时间日期

相比旧的时间日期:

  • 更清晰的 API 设计:之前使用的java.util.Date月份从0开始,我们一般会+1使用,很不方便。java.time.LocalDate提供了自然、简洁的方式来操作日期和时间。
  • 线程安全/不可变:java.util.Date和SimpleDateFormat都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型不但线程安全,而且不能修改
  • 支持日期和时间计算:新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。

问:JDK1.7与JDK1.8 ConcurrentHashMap对比 ?

  • 1.7采用segment的分段锁机制实现线程安全;1.8则摒弃了分段锁,采用CAS+Synchronized保证线程安全。
  • 1.7使用了结构为ReentrantLock+Segment+HashEntry(数组),分段哈希表,每个段内部是一个链表;1.8则摒弃了分段锁,结构为synchronized+CAS+HashEntry(数组)+红黑树。内部结构使用了桶(Bucket)和树(Tree)结构,链表长度超过阈值后转化为红黑树。
  • 查询时间复杂度从原来的遍历链表O(n),变成遍历红黑树O(logN),改进了性能。
  • 1.8 HashMap数组+链表+红黑树来实现hashmap,当碰撞的元素个数大于8时 & 总容量大于64,会有红黑树的引入。除了添加之后,效率都比链表高,1.8之后链表新进元素加到末尾。

问:JDK1.8使用synchronized来代替重入锁ReentrantLock ?

红黑树的节点操作(插入、删除)使用 synchronized 来确保线程安全。在 JDK 1.8 中,ConcurrentHashMap 内部的桶(Bucket)是用 synchronized 进行同步的。每个桶是一个链表或红黑树,链表中的节点在插入或删除时使用 synchronized 来保证线程安全。当桶中的链表长度超过一定阈值(默认为 8),链表会被转换成红黑树。

为什么使用 synchronized 而不是 ReentrantLock

  • 锁粒度降低:synchronized是JVM内置的锁,相比ReentrantLock 更轻量,性能更高效,基于JVM的synchronized优化空间更大。
  • ReentrantLock有更多功能,如尝试锁、定时锁等,但对于ConcurrentHashMap并不需要。
  • 在大数据量下,基于API的ReentrantLock会比基于JVM的内存压力开销更多的内存

十. Java 9 新特性

问:Java 9引入了哪些新特性?

  • 模块化系统(Project Jigsaw)

    • 模块系统:使得 Java 平台可以被拆分成多个模块。每个模块都可以定义自己的依赖关系,导出和隐藏自己的包。使得 Java 应用更加模块化和可维护。

    • 关键组件module-info.java 文件是模块的描述文件,定义了模块的名称、导出的包以及需要的模块等。

      示例:

      1
      2
      3
      4
      module com.example.myapp {
      requires java.sql;
      exports com.example.myapp.services;
      }
  • JShell:Java REPL

    • JShell:Java 9 引入了一个新的命令行工具 JShell,作为 Java 的 REPL(Read-Eval-Print Loop)工具。它允许开发者快速测试 Java 代码片段,无需编写完整的程序。

      示例:

      1
      2
      3
      4
      5
      $ jshell
      jshell> int x = 5;
      x ==> 5
      jshell> x + 10
      $1 ==> 15
  • 增强的 Stream API

    • 新方法:对 Stream API 进行了增强,新增了一些方法,如 takeWhile, dropWhile, 和 ofNullable

      示例:

      1
      2
      3
      4
      List<String> list = Arrays.asList("apple", "banana", "cherry", "date");
      list.stream()
      .dropWhile(s -> s.startsWith("a"))
      .forEach(System.out::println); // Outputs: banana, cherry, date
  • 改进的 Optional API

    • 新方法Optional 类新增了 ifPresentOrElseor 方法,用于更灵活地处理缺失值的情况。

      示例:

      1
      2
      3
      4
      5
      Optional<String> optional = Optional.of("Hello");
      optional.ifPresentOrElse(
      value -> System.out.println("Value: " + value),
      () -> System.out.println("No value present")
      );
  • 私有方法的接口

    • 私有方法:Java 9 允许在接口中定义私有方法,以便于接口的默认方法和静态方法可以共享代码。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      interface MyInterface {
      private void privateMethod() {
      // Implementation
      }

      default void defaultMethod() {
      privateMethod();
      }
      }
  • 改进的 @SuppressWarnings 注解

    • 参数化:可以在 @SuppressWarnings 注解中使用新的参数,以便更细粒度地控制警告的抑制。

    • 示例:

      1
      2
      3
      4
      @SuppressWarnings({"unchecked", "deprecation"})
      public void myMethod() {
      // Code
      }
  • 堆外内存的改进

    • 改进的 Unsafe API:引入了新的 API 来更安全地操作堆外内存(Direct Memory),提供了对 Unsafe 的更加受限的访问方式。
  • 改进的 java.util 包,集合的工厂方法

    • List.ofMap.of 方法:新增了简便的方法来创建不可变的集合,如 List.of, Map.of, 和 Set.of

      示例:

      1
      2
      List<String> list = List.of("a", "b", "c");
      Map<String, Integer> map = Map.of("key1", 1, "key2", 2);
  • 性能改进和 JDK 内部改进

    • 性能优化:包括对 JIT 编译器、垃圾回收和其他运行时性能的改进。
    • 内部 API 的访问控制:Java 9 引入了更严格的访问控制,限制了对 JDK 内部 API 的访问。

问:模块化系统 ?

模块是一个包的容器,Java 9 最大的变化之一是引入了模块系统(Jigsaw 项目),旨在改进代码的结构化和可维护性,主要通过 module-info.java 文件来实现模块化开发。

  • 每个模块都包含一个名为 module-info.java 的描述文件,定义了模块的名称、导出的包、需要的模块等。

    1
    2
    3
    4
    module com.example.myapp {
    requires java.sql; // 依赖于 java.sql 模块
    exports com.example.myapp.services; // 导出 com.example.myapp.services 包
    }
  • 指令:

    • 导出(Exports):指定哪些包对其他模块可见。
    • 开放(Opens):允许其他模块通过反射访问模块中的包。
    • 需要(Requires):指定当前模块所依赖的其他模块。
    • 使用(Uses):指定当前模块使用的服务类型。服务提供者通过 ServiceLoader 机制来实现。
    • 提供(Provides):指定当前模块提供的服务实现。

问:集合工厂方法 ?

通常,您希望在代码中创建一个集合(例如,List 或 Set ),并直接用一些元素填充它。 实例化集合,几个 “add” 调用,使得代码重复。 Java 9,添加了几种集合工厂方法:

1
2
Set<Integer> ints = Set.of(123);
List<String> strings = List.of("first""second");

问:改进的 Stream API ?

Stream 接口中添加了 4 个新的方法:dropWhile, takeWhile, ofNullable。还有个 iterate 方法的新重载方法

问:改进的 Javadoc ?

Javadoc 现在支持在 API 文档中的进行搜索。另外,Javadoc 的输出现在符合兼容 HTML5 标准。

  • HTML5 支持:Javadoc 默认生成 HTML5 格式的文档,取代了过去使用的 HTML4。这使得生成的文档更加现代化,兼容性更好,同时支持更丰富的 HTML 特性。
  • 内置搜索功能:Java 9 为 Javadoc 文档新增了一个内置的搜索框,使开发者能够在文档中搜索类、接口、方法和字段。这极大提升了查找特定信息的效率,避免了手动浏览所有页面。
  • 模块化支持:Java 9 引入了 Java 平台模块系统(Jigsaw 项目),Javadoc 工具增加了对模块的支持。开发者可以为模块生成文档,文档中清晰展示了模块及其依赖关系。这对于大型项目或使用模块化结构的应用程序特别有用。
    • 使用 javadoc --module 参数可以为指定模块生成文档。
    • 生成的文档中展示了模块依赖和导出的 API 信息。
  • 等等。

十一. Java 11 特性

redis代理集群模式,三次握手,valitile重排序底层代码, cas 事务的4个特性,filter和interceptor的区别 @autowired原理, dispatcherservlet,分布式事务解决方案spring都有哪些模块,fork join队列,排序算法,