Java面试提问准备

文章目录

补充的内容是为了方便理解。
2024.6.10开始,已更新至6.30。

一.Java基础知识

1.接口和抽象类的区别

  • 相似点:
    1. 接口和抽象类都不能被实例化。
    2. 实现接口的普通子类或继承抽象类的普通子类都必须实现其中的所有抽象方法。
  • 不同点:
    1. 抽象类可以包含普通方法和代码块,接口里只能包含抽象方法,静态方法和默认方法。
    2. 抽象类可以有构造方法,接口没有。
    3. 抽象类的成员变量可以是各种类型的,接口的成员变量只能是public static final类型的,即常量,并且必须赋值。

补充:

  • 实例:在面向对象编程(OOP)中,“实例”指的是一个具体的对象,它是一个类的具体实现。在编程中,使用一个类创建一个对象时,这个对象就是类的一个实例。实例是类的实际存在的对象,包含类中定义的属性和方法,并且可以对这些属性赋值和调用方法。

    • 类(Class):类是一个蓝图或模板,它定义了一组属性(字段)和行为(方法)。类本身是抽象的,它描述了对象的共同特征,但不是一个实际的对象。
    • 实例(Instance):实例是根据类创建的具体对象。实例是实际存在于内存中的实体,你可以操作实例的属性和调用它的方法。
  • 实例化:接口和抽象类都不能被实例化,这里的“实例化”指的是不能直接创建这些类型的对象。换句话说,不能使用 new 关键字直接创建接口或抽象类的实例。说明如下:

    • 接口:接口定义了一组方法,但不包含任何实现细节。因此,接口不能直接创建对象。可以定义一个接口并让一个具体类实现它,然后创建该具体类的实例。
    • 抽象类:抽象类可以包含具体方法的实现,但它们也可以包含抽象方法(没有实现的方法)。因为抽象类的目的是被其他类继承并实现其抽象方法,所以抽象类不能直接创建实例。
  • 在Java中,接口的所有字段都是隐式地 public static final,因此我们不能在接口中声明一个没有初始化的实例变量。接口中的成员变量隐式声明:

    double PI = 3.1415926; // 等同于 public static final double PI = 3.1415926;
    
  • 接口适用于定义契约和行为的多继承,提供多态性和解耦实现的能力。

  • 抽象类适用于提供部分默认实现、共享代码、定义模板方法以及限制实例化和扩展的需求

2.重载与重写的区别

重载(Overloading)和重写(Overriding)是Java中的两种多态性实现方式,它们在方法的定义和使用上有不同的意义和用途。

重载:同一个类中,允许存在多个同名方法,用于提高方法的可读性和可维护性。

重载发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同

重写:子类重新定义父类中已经存在的方法。重写用于实现子类特定的行为。

重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中, 方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。

总的来说,方法提供的行为改变,而方法的外貌并没有改变。

补充:

  1. 方法提供的行为改变,而方法的外貌并没有改变:指的是方法的签名(包括方法名、参数列表、返回类型)保持不变。子类方法的名称和参数列表与父类方法完全相同,看起来就像是同一个方法。但是方法的具体实现(即方法体)在子类中发生了变化。

​ 注意:尽管在子类中的方法看起来与父类中的方法完全相同,但如果它们提供了不同的行为,那么它们是重写而不是重载。在Java中,重写是基于继承和多态的概念,它允许子类提供对父类方法的新实现。

3. == 与equals的区别

==比较基本类型时,比较的是值。比较引用类型时,比较的是内存地址。

equals是Object类的方法,默认比较的是引用对象的内存地址。本质上与==一样,但是有一些类重写了equals方法,比如String的equals被重写后,比较的是字符串的内容是否相等。

另外,重写了equals后也必须重写hashcode方法。

  • 重写 equals 方法时必须重写 hashCode 方法:这是为了保证在使用哈希表(如 HashMapHashSet)时的正确性。根据 Java 的规范,如果两个对象根据 equals 方法比较是相等的,那么它们的 hashCode 值也必须相同。

4.异常处理机制

  1. 使用try,catch,finally捕获异常,finally中的代码一定会执行,捕获异常后程序会继续执行。
  2. 使用throws声明该方法可能出现的异常类型,出现异常后,程序终止。

补充:

  1. finally中的代码一定会执行,大部分情况下这是对的,但是在以下两种情况下不成立。
    • 如果在 trycatch 块中调用了 System.exit() 或发生了 JVM 崩溃等极端情况,finally 块中的代码可能不会执行。
    • 捕获异常后程序继续执行:如果异常被 catch 块成功捕获并处理,程序会继续执行 catch 块后面的代码(包括 finally 块中的代码)。如果异常没有被捕获(没有相应的 catch 块处理该异常),程序会终止并抛出异常。
  2. System.exit():终止正在运行的Java虚拟机(JVM)
  3. 使用 throw 抛出异常时,方法会立即终止并将控制权转移到调用栈中的第一个能够处理该异常的 catch 块。如果该异常没有被捕获(即没有相应的 try-catch 块处理该异常),程序将会终止并打印堆栈跟踪信息。如果异常被捕获并处理,程序不会终止。
    • throw 关键字用于显式抛出一个异常。抛出异常时,方法会立即终止,控制权转移到调用栈中的第一个能够处理该异常的 catch 块。
    • throws 关键字用于在方法签名中声明该方法可能抛出的异常类型,这样调用该方法的代码必须处理这些异常(通过 try-catch 块)或继续声明抛出。

示例代码

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            methodThatThrowsException();
        } catch (Exception e) {
            System.out.println("Exception caught: " + e.getMessage());
        }
        System.out.println("After method call");
    }

    // 声明这个方法可能抛出 Exception
    public static void methodThatThrowsException() throws Exception {
        // 显式抛出一个 Exception
        throw new Exception("An exception occurred");
    }
}

输出结果

Exception caught: An exception occurred
After method call

5.自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来。例如,将 int 类型转换为 Integer 类型。

拆箱:将包装类型转换为基本数据类型。

// 装箱:将基本类型转换为包装类型
int primitiveInt = 10;
Integer boxedInt = Integer.valueOf(primitiveInt); // 手动装箱
Integer autoBoxedInt = primitiveInt; // 自动装箱

// 拆箱:将包装类型转换为基本类型
Integer wrapperInt = new Integer(20);
int unboxedInt = wrapperInt.intValue(); // 手动拆箱
int autoUnboxedInt = wrapperInt; // 自动拆箱

6.HashMap存储原理

  1. 计算key的hash值,使用key的hashCode()方法。

  2. 然后进行二次hash(或扰动函数),这是为了减少hash冲突,提高分布均匀性。

    # JDK 8中的具体实现是:
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  3. 根据二次hash结果找到对应的索引位置。索引位置通过对数组长度取模运算(一般是使用位运算与操作来实现)的方式确定:

    int index = (n - 1) & hash;
    
  4. 如果计算出的索引位置已经有节点(即发生了hash冲突),则需要遍历链表或者树来找到是否存在相同的key。

    • 如果找到了相同的key(通过equals方法比较),则替换掉旧的value。
    • 如果没有找到相同的key,则在链表末尾插入新的节点。使用高低位平移法将节点插入链表。(JDK8以前使用头插法,但是头插法在并发扩容是可能会造成环形链表或数据丢失,而高低位平移法会发生数据覆盖的情况)(JDK 8中如果链表长度超过一定阈值会转换为红黑树,以提高查找效率)。

    补充:

    头插法(JDK 7及以前)

    newNode.next = table[index];
    table[index] = newNode;
    

    尾插法(JDK 8)

    if (first == null) {
        table[index] = newNode;
    } else {
        Node<K, V> e;
        do {
            e = last;
        } while ((last = last.next) != null);
        e.next = newNode;
    }
    
  • 树化:JDK8中如果某个桶内的链表长度超过阈值(默认是8),则链表会转换为红黑树,以提高查找和插入的性能。
  • 扩容:当HashMap中的元素超过阈值(容量*负载因子)时,会进行扩容。扩容时,所有节点会重新计算索引并分布到新的更大的数据中。
  • 哈希表中的二次哈希:是一种解决哈希冲突的技术,通过使用第二个哈希函数来计算步长,从而避免冲突位置。
  • Java HashMap 中的扰动函数:用于进一步混淆和分散哈希值,以减少冲突,提高哈希表的性能。
  • hash冲突:首先,计算出的key的hash值,再通过二次hash(位运算)定位的是一个哈希桶数组(bucket array),桶中存储链表/树的头节点或根节点,如果多个hash值相同,即发生了hash冲突,再计算key的索引,定位的是在哪个哈希桶中的哪个链表/树的头节点或根节点,再进行遍历操作,如果存在这个key,替换value,不存在,进行插入操作。
    • 初始状态:HashMap的底层是一个数组,称为哈希桶数组(bucket array),每个槽位(bucket)可以存放一个链表的头节点或红黑树的根节点。初始状态下,这些槽位都是空的,即为 null

7.面向对象与面向过程的区别

面向对象编程(Object-Oriented Programming, OOP)和面向过程编程(Procedural Programming)是两种不同的编程范式,它们在程序的组织方式、思想和方法上存在显著区别。面向对象有封装、继承、多态性的特性,所以相比面向过程易维护、易复用、易扩展,但是因为类调用时要实例化,所以开销大性能比面向过程低。

补充:

  1. 编程思想

面向对象编程

  • 核心概念:对象和类。
  • 基本思想:通过将数据和操作数据的代码封装在对象中,强调对象之间的交互。
  • 主要特性
    • 封装:将数据和方法封装在类中,隐藏实现细节,只对外提供接口。
    • 继承:通过继承机制实现代码复用,建立类之间的层次结构。
    • 多态:通过方法重写和接口实现,使得同一个操作可以作用于不同的对象上,实现灵活的代码设计。

面向过程编程

  • 核心概念:过程(函数)和模块。
  • 基本思想:通过将程序分解成一个个过程或函数,强调过程的顺序执行。
  • 主要特性
    • 模块化:将程序分解成若干个模块,每个模块实现特定的功能。
    • 自顶向下设计:从顶层开始逐步细化,逐层实现具体功能。
    • 代码复用:通过函数的调用来实现代码复用。
  1. 组织方式

面向对象编程

  • 程序由类和对象组成。类是对象的模板,对象是类的实例。
  • 数据和操作数据的方法被封装在对象中。
  • 强调对象之间的消息传递和协作。

面向过程编程

  • 程序由函数和过程组成。
  • 数据和操作数据的代码是分离的。
  • 强调函数调用的顺序和数据在函数之间的传递。
  1. 代码复用

面向对象编程

  • 通过继承和多态实现代码复用。
  • 可以通过组合和聚合复用代码。

面向过程编程

  • 通过函数调用实现代码复用。
  • 代码复用依赖于模块的设计和函数的调用。
  1. 适用场景

面向对象编程

  • 适用于复杂系统的开发,如图形用户界面(GUI)、游戏开发、大型软件系统等。
  • 更适合需要频繁维护和扩展的项目。

面向过程编程

  • 适用于计算密集型任务、简单的脚本和小型程序。
  • 更适合对性能要求较高的底层系统开发,如操作系统、嵌入式系统等。

总结

  • 面向对象编程:通过对象和类的概念,强调数据和方法的封装、继承和多态,适合处理复杂和需要扩展的系统。
  • 面向过程编程:通过函数和过程的概念,强调过程的顺序执行和模块化,适合处理简单的任务和对性能要求高的系统。

8.深拷贝和浅拷贝

浅拷贝:只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存。

深拷贝:会创造一个一模一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对象。

9.多态的作用

多态的实现三要素:继承,重写,父类引用指向子类对象(使得调用父类方法时,实际执行的是子类的方法)。它的好处是可以消除类型之间的耦合关系,增加类的可扩展性和灵活性。

10.什么是反射?

反射是Java中的一个特性,允许程序在运行时获取有关类的详细信息,并动态地操作类的属性和方法。通过反射,可以在运行时访问类的内部结构,比如获取类的字段(属性)、方法、构造器等,并对其进行操作。

应用场景:要操作权限不够的类属性和方法时,实现自定义注解时,动态加载第三方jar包时,按需加载类,节省编译和初始化时间,框架和库的实现:许多Java框架(如Spring、Hibernate)使用反射来进行依赖注入、持久化和代理等功能。

获取class对象的方法有:

  • Class.forName(String className):通过类的全限定名(包名+类名)获取类的Class对象。

    Class<?> clazz = Class.forName("com.example.MyClass");
    
  • 类名.class:通过类名的class属性获取Class对象。

    Class<MyClass> clazz = MyClass.class;
    
  • 对象的getClass()方法:通过对象的getClass()方法获取Class对象

    MyClass myObject = new MyClass();
    Class<? extends MyClass> clazz = myObject.getClass();
    

补充:

简单的反射示例,如何获取类的字段,方法,并进行调用

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) {
        try {
            // 获取Class对象
            Class<?> clazz = Class.forName("com.example.MyClass");

            // 创建类的实例
            Object obj = clazz.getDeclaredConstructor().newInstance();

            // 获取私有字段
            Field field = clazz.getDeclaredField("privateField");
            field.setAccessible(true); // 绕过访问控制检查
            field.set(obj, "New Value");

            // 获取方法
            Method method = clazz.getDeclaredMethod("privateMethod");
            method.setAccessible(true); // 绕过访问控制检查
            method.invoke(obj);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    private String privateField = "Initial Value";

    private void privateMethod() {
        System.out.println("Private method invoked");
    }
}

11.Java创建对象的五种方式

  1. new关键字
  2. Class.newInstance(这种方法使用无参构造函数来创建对象实例。它已经被弃用,因为它没有提供对构造函数的参数化支持,且会抛出一些额外的异常。)
  3. Constructor.newInstance(推荐,支持带参数的构造函数)
  4. Clone方法(创造当前对象的一个副本)
  5. 反序列化(从二进制数据中恢复对象的过程,需要实现Serializable接口)

补充:

  • 从二进制数据中恢复对象的过程:指的是将一个已经被序列化(即转换为二进制数据流)的对象重新转换为内存中的对象的过程,这个过程称为反序列化。
  • 序列化(Serialization):将对象的状态转换为一个字节流,以便将对象的状态保存到文件、内存、数据库,或者通过网络传输到另一个系统。
  • 反序列化(Deserialization):将字节流恢复为对象的过程,即从二进制数据中重建对象。

12.Java语言特点

优点较多,突出特点有以下四个。

  • 面向对象(封装,继承,多态)
  • 平台无关性:具体表现在于,Java是一次编写,随处运行的语言,因此采用Java语言编写的程序具有很好的可移植性,保障这一点的是JVM机制。引入JVM后,Java语言在不同平台上运行不需要重新编译。
  • 支持多线程:C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程支持。
  • 编译与解释并存。

13.HashTable与HashMap的区别

  1. HashTable的每个方法都用synchronized修饰所以是线程安全的,但同时读写效率很低。
  2. HashTable的键或值不允许为null,HashMap允许一个null键和多个null值。
  3. HashTable只对key进行一次hash,HashMap进行两次Hash。
  4. HashTable和HashMap底层都使用的数组加链表。在 Java 8 及之后的版本中,当链表长度超过一定阈值时,HashMap 使用红黑树代替链表。

补充:

  • 唯一的 nullHashMap 允许一个 null 键,多个 null 键会覆盖之前的 null 键,因为键是唯一的。
  • 多个 nullHashMap 允许多个 null 值,不同的键可以映射到 null 值。
  • NullPointerException:当你使用 null 键进行某些操作时,比如自定义的 hashCode 方法或 equals 方法,如果这些方法不处理 null 值,可能会抛出 NullPointerException

14.说说List,Set,Map三者的区别

List 接口存储一组有序的元素,可以包含重复的元素。List 中的元素可以通过索引访问。

Set 接口存储一组不重复的元素,元素是无序的。Set 不允许存储重复的元素(即不会有两个元素引用相同的对象)

Map 接口存储键值对(key-value),每个键唯一,键不可重复,但可以有相同的值。Map 通过键来查找对应的值。

15.什么是线程死锁

线程死锁(Deadlock)是一种特定的资源竞争情况,发生在两个或多个线程互相等待对方释放资源,从而导致这几个线程都无法继续执行的状态。简单来说,死锁就是每个线程都在等待其他线程持有的资源,导致所有线程都无法继续执行。

要发生死锁,必须同时满足以下四个条件:

  1. 互斥条件:至少有一个资源是不能被共享的,即一次只能被一个线程占有。
  2. 持有并等待条件:至少有一个线程持有了一个资源,并且正在等待获取另外一个被其他线程占有的资源。
  3. 不可剥夺条件:资源不能被强制从一个线程中剥夺,只能由持有它的线程显式地释放。
  4. 环路等待条件:存在一个线程集合 {T1, T2, ..., Tn},其中 T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,依此类推,Tn 等待 T1 持有的资源,形成一个等待环路。

16.如何避免线程死锁

破坏请求与保持条件:一次性申请所有的资源。

破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源的时候,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源时,如果申请不到,可以主动释放它占有的资源。

17.说说sleep()和wait()的区别和共同点

两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁。

两者都可以暂停线程的执行。

wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。

wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程后自动苏醒。

补充:

notifynotifyAll 方法

  • notify():唤醒一个正在等待该对象监视器的线程。如果有多个线程在等待这个对象的监视器,则其中一个线程会被唤醒,具体选择哪个线程由 JVM 决定。被唤醒的线程需要重新获得对象的锁才能继续执行。
  • notifyAll():唤醒所有正在等待该对象监视器的线程。所有被唤醒的线程都需要重新获得对象的锁才能继续执行。

18.HashMap和ConcurrentHashMap的区别

  1. HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。
  2. ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成几个小的片段segment,而且每个小的片段segment上面都有锁,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。
  3. ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

补充:

  • 锁分段技术(Segment locking):在 Java 7 及之前,ConcurrentHashMap 使用了锁分段技术(Segment locking)。它将整个哈希表分为多个 Segment,每个 Segment 独立加锁,从而提高并发性能。
  • CAS 和 Synchronized:在 Java 8 中,ConcurrentHashMap 摒弃了 Segment,转而使用了一种基于 CAS(Compare-And-Swap)和 Synchronized 锁的混合机制。对于常见的读操作,使用 CAS 来实现无锁操作;对于写操作,则在必要时使用 Synchronized 来保证线程安全。
  • 锁粒度:通过分段锁或更细粒度的锁机制,允许多个线程并发地访问不同的部分,从而提高并发性能。例如,在 Java 8 中,ConcurrentHashMap 使用的树化结构(当桶中元素过多时,将链表转为红黑树)也提高了在高并发环境下的性能。

19.现在有线程T1,T2,T3.如何确保T2线程在T1之后执行,并且T3线程在T2之后执行?

可以用Thread类的join方法实现这个效果。

补充:

  1. 使用join()方法:
Thread t1 = new Thread(() -> {
    // T1的任务
});

Thread t2 = new Thread(() -> {
    // T2的任务
});

Thread t3 = new Thread(() -> {
    // T3的任务
});

t1.start();
t1.join();  // 等待T1完成

t2.start();
t2.join();  // 等待T2完成

t3.start();
  1. 使用CountDownLatch:CountDownLatch是一个同步辅助类,可以让一个或多个线程等待直到一组操作在其他线程中完成。
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    // T1的任务
    latch1.countDown();
});

Thread t2 = new Thread(() -> {
    latch1.await();  // 等待T1完成
    // T2的任务
    latch2.countDown();
});

Thread t3 = new Thread(() -> {
    latch2.await();  // 等待T2完成
    // T3的任务
});

t1.start();
t2.start();
t3.start();

这两种方法都可以确保T2在T1之后执行,T3在T2之后执行。选择哪种方法取决于具体的应用场景和需求。

二.Java核心相关

1.构造器Constructor是否可被override?

Constructor 不能被 override(重写),但是可以 overload(重载),所以可以看到一个类中有多个构造函数的情况。

补充:

  1. 构造器(构造方法)是用于初始化对象的特殊方法。它在创建对象时被调用,用于设置对象的初始状态。构造器的名字必须与类名相同,并且没有返回类型(即使是 void 也不能有)。

    主要特性:

    1. 名字与类名相同:构造器的名字必须与类名完全相同。
    2. 没有返回类型:构造器没有返回类型,甚至不能写 void
    3. 可以有参数:构造器可以有参数,用于初始化对象时传递必要的信息。
    4. 可以有多个:类可以有多个构造器(构造方法重载),每个构造器有不同的参数列表。
    5. 默认构造器:如果类没有定义任何构造器,编译器会提供一个默认的无参构造器。

2.String为什么是不可变的?

String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。

补充:

在Java 8及以前的版本中,String 类使用 final 关键字修饰字符数组来保存字符串。这是使 String 对象不可变的一个关键因素。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 用 final 修饰的字符数组来保存字符串
    private final char value[];
}

由于 value 是用 final 修饰的,所以一旦 String 对象被创建,value 数组的引用不能被更改。此外,String 类的方法不会修改 value 数组的内容,因此 String 对象是不可变的。

从Java 9开始,String 类的内部实现进行了优化。它使用了一个由字节数组 byte[] 和一个编码标志 coder 组成的新的内部表示,以减少内存使用。这种新的表示方式称为“紧凑字符串”(Compact Strings)。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 用 final 修饰的字节数组来保存字符串
    private final byte[] value;

    // 编码标志
    private final byte coder;
}

尽管内部实现发生了变化,但 String 对象的不可变性仍然得到了保证。value 数组和 coder 都是用 final 修饰的,保证了它们的引用不能被更改。并且 String 类的方法不会修改 value 数组的内容。

3.String,StringBuffer和StringBuilder的区别是什么?

可变性:

String类中使用final关键字修饰字符数组来保存字符串,private final char[] value,因此String对象是不可变的。不可变性意味着一旦创建后,String对象的内容不能被改变。

StringBuilder和StringBuffer都继承自AbstractStringBuilder类。在AbstractStringBuilder中,也使用字符数组保存字符串(char[] value),但是没有使用final关键字修饰,因此这两种对象是可变的。

线程安全性:

StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

性能:

每次对String类型进行改变时,都会生成一个新的String对象,然后将指针指向新的String对象,这会导致频繁的对象创建和销毁。

StringBuffer每次都对自身进行操作,而不是生成新的对象并改变对象引用,因此在多次修改字符串内容时性能更好。

在单线程环境下,相比于StringBuffer,使用StringBuilder能够获得10%~15%左右的性能提升,但在多线程环境下使用StringBuilder会带来线程安全问题。

使用总结:

操作少量数据:使用String,因为它的不可变性和字符串常量池机制能够带来方便和效率。

单线程环境下操作大量数据:使用StringBuilder,因为它在没有同步开销的情况下提供更好的性能。

多线程环境下操作大量数据:使用StringBuffer,因为它是线程安全的。

4.线程有哪些基本状态,并描述它们。

状态名称说明
NEW初始状态,线程被构建,但是还没有调用start()方法
BUNNABLE运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中”
BLOCKED阻塞状态,表示线程阻塞于锁
WAITING等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程的一些特定动作(通知或中断)
TIME_WAITING超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED终止状态,表示当前线程已经执行完毕

5.final关键字修饰这三个地方:变量,方法,类,会有什么作用

final变量:如果是基本类型的变量,则其数值一旦在初始化之后便不能更改。如果是引用类型的变量,则在其初始化后便不能再让其指向另一个对象。

final修饰的方法:

  1. 把方法锁定,以防任何继承类修改它的含义。
  2. 提升性能,现代JVM会进行多种优化,但声明为final的方法有可能使JVM更容易进行某些优化。因为JVM知道这些方法不能被重写,所以可以在编译时进行内联优化(inline optimization),这有时会提升性能。

final修饰的类:表明这个类不能被继承。其类中的所有成员方法都会被隐式地指定为final。

6.Java序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。

transient关键字的作用是:阻止实例中使用此关键字修饰的变量序列化。当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。

transient只能修饰变量,不能修饰类和方法。

7.获取键盘输入,常用的两种方法

  1. Scanner

    import java.util.Scanner;
    
    public class ScannerExample {
        public static void main(String[] args) {
            Scanner input = new Scanner(System.in);
            System.out.println("Enter a line of text:");
            String s = input.nextLine();
            System.out.println("You entered: " + s);
            input.close();
        }
    }
    
  2. BuffererReader

    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.IOException;
    
    public class BufferedReaderExample {
        public static void main(String[] args) {
            BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Enter a line of text:");
            try {
                String s = input.readLine();
                System.out.println("You entered: " + s);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

8.既然有了字节流,为什么还要有字符流?

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

字符流是由JVM将字节转换得到的,问题在于这个过程是非常耗时的,并且,如果我们不知道编码类型就很容易出现乱码问题。所以,I/O流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果是音频文件,图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

补充:

  • 字节流 (Byte Stream)

    • 操作单位:字节 (8 bits)

    • InputStreamOutputStream 及其子类,如 FileInputStreamFileOutputStreamBufferedInputStreamBufferedOutputStream 等。

    • 适用场景:主要用于处理二进制数据,如音频、图片、视频等文件。在这种场景下,数据不需要进行编码转换,直接进行字节级别的读写操作。

  • 字符流 (Character Stream)

    • 操作单位:字符 (16 bits,即 2 bytes,用于表示 Unicode 字符)
    • ReaderWriter 及其子类,如 FileReaderFileWriterBufferedReaderBufferedWriter 等。
    • 适用场景:主要用于处理文本数据(如普通文本文件),字符流会自动处理字符编码问题,可以直接操作字符数据,更加方便。
  • 字节流与字符流的选择

    • 字节流:适用于处理所有类型的文件数据,包括二进制文件(如图片、音频、视频)和文本文件。字节流不涉及字符编码问题,读写操作直接基于字节。
    • 字符流:专门用于处理文本文件,因为它们能够自动处理字符编码和解码,使文本处理更加方便。在处理文本数据时,使用字符流可以避免手动编码转换和潜在的编码问题。

9.&与&&的区别

&和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为ture,否则,只要有一方为false,则结果为false。

&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式。

&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,它逐位比较两个整数的二进制位,并且只有当相应的两位都为 1 时,结果位才为 1,否则结果位为 0。例如,0x31 & 0x0f的结果为0x01.

10.Collection与Collections区别

CollectionCollections 是 Java 中两个不同的概念,它们在功能和用途上有明显的区别。以下是对它们的详细解释:

Collection:

  • 定义Collection 是 Java 中集合框架的根接口。它是所有单一集合类型(如列表、集合和队列等)的最基本接口。Collection 不直接提供具体实现,而是定义了一组通用操作,具体的集合类如 ArrayListHashSetLinkedList 等都实现了这个接口。
  • 所在包java.util.Collection
  • 子接口ListSetQueue
  • 常用方法
    • add(E e): 向集合中添加元素
    • remove(Object o): 从集合中移除元素
    • size(): 返回集合中元素的数量
    • isEmpty(): 判断集合是否为空
    • iterator(): 返回集合的迭代器
    • contains(Object o): 判断集合是否包含某个元素
    • clear(): 清空集合中的所有元素

Collections:

  • 定义Collections 是一个工具类,包含了一些操作或返回集合的静态方法。这些方法用于对集合进行排序、搜索、填充、同步化等操作。
  • 所在包java.util.Collections
  • 常用方法
    • sort(List<T> list): 对列表进行排序
    • reverse(List<?> list): 反转列表中的元素顺序
    • shuffle(List<?> list): 随机打乱列表中的元素
    • binarySearch(List<? extends Comparable<? super T>> list, T key): 对排序列表进行二分搜索
    • max(Collection<? extends T> coll): 返回集合中的最大元素
    • min(Collection<? extends T> coll): 返回集合中的最小元素
    • synchronizedCollection(Collection<T> c): 返回同步的(线程安全的)集合
    • unmodifiableCollection(Collection<? extends T> c): 返回不可修改的集合

总结

  • Collection 是一个接口,用于表示一组对象,是集合框架的根接口,定义了集合的基本操作。
  • Collections 是一个工具类,包含了一系列静态方法,用于操作或返回集合,如排序、搜索和同步化等。

11.final,finally,finalize的区别

final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义为final类型。

finally是异常处理语句结构的一部分,表示总是执行。

finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。但是JVM不保证此方法总被调用。因此,不应该依赖 finalize() 方法来执行重要的资源回收或清理工作。建议使用显式的资源管理技术,如 try-with-resources(用于实现 AutoCloseable 接口的对象),或者显式调用 close() 方法来管理资源。

12.ArrayList和Vector的区别

这两个类都实现了List接口(List接口继承了Collection接口),它们都是有序集合,即储存在这两个集合中的元素的位置都是有顺序的。

(1)同步性:

Vector是线程安全的,也就是说,它的方法之间是线程同步的,而ArrayList是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList,因为它不考虑线程安全,效率会高些。如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

(2)数据增长:

ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素个数超过了容量时,就需要增加存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。

  • ArrayList:当需要扩容时,默认增加 50% 的容量(即当前容量的 1.5 倍)。

  • Vector:当需要扩容时,默认增加 100% 的容量(即当前容量的 2 倍)。不过,这个增长量也可以通过构造函数指定。

补充:

  • 遗留问题Vector 是一个较老的集合类,早在 Java 1.0 中就引入了。自从引入 ArrayList 后,Vector 使用变得不那么普遍了,主要因为 ArrayList 提供了更好的性能和更灵活的同步机制。

  • 在多线程环境中,如果需要线程安全的集合,可以使用 Vector 或同步的 ArrayList

  • 同步的ArrayList示例

    List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
    synchronizedList.add(1);
    synchronizedList.add(2);
    synchronizedList.add(3);
    

    Vector示例

    Vector<Integer> vector = new Vector<>();
    vector.add(1);
    vector.add(2);
    vector.add(3);
    

13.ArrayList和LinkedList的区别

ArrayList的底层使用动态数组,默认容量为10。容量达到上限时,会生成一个新的数组,大小通常是之前的1.5倍。因为数组在内存中是连续的地址,所以ArrayList查找数据更快,时间复杂度为O(1)。由于扩容机制,添加数据的效率在需要扩容时会降低。此外,插入和删除数据需要移动元素,时间复杂度为O(n)。

LinkedList的底层使用双向链表,在内存中是非连续的,没有扩容机制。LinkedList在查找数据时需要从头遍历或从尾遍历,时间复杂度为O(n)。但是添加、插入和删除数据时,修改指针即可,效率更高,时间复杂度为O(1)(前提是已知节点的位置)

三.JavaWeb相关

1.get和post请求的区别

  • Get请求只能URL编码,而POST支持多种编码格式。
  • Get请求只接受ASCLL字符的参数,而POST则没有限制。
  • Get请求的参数通过URL传送,而POST放在Request Body中。
  • Get相对于Post更不安全,因为参数直接暴露在URL中。
  • Get请求会被浏览器主动缓存,而POST不会(除非手动设置)
  • Get请求在URL传参有长度限制,而POST没有限制。
  • Get产生的地址可以被收藏,而POST不可以。
  • Get请求的参数会被完整的保留在浏览器的历史记录里,而POST的参数则不会。
  • Get在浏览器回退时是无害的,而POST会再次提交请求。

2.转发(Forward)和重定向(Redirect)的区别

转发是服务器行为,重定向是客户端行为。

从地址栏显示来说:

  • forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取出来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内从哪里来的,所以它的地址栏还是原来的地址。
  • redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的URL。

从数据共享上来说:

  • forward:转发页面和转发到的页面可以共享request里面的数据
  • redirect:不能共享数据。(转发是在服务器中进行的,数据共享,重定向是通过

从运用方法上来说:

  • fowrad:一般用于用户登录的时候,根据角色转发到相应的模块。
  • redirect:一般用于用户注销登陆时返回页面和跳转其他的网站等。

从效率来说:

  • forward高,redirect低。

3.JSP和Servlet是什么关系

Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供动态生成的内容。相比之下,JSP(JavaServer Pages)是一种简化的Servlet开发方式,它允许将静态内容(如HTML)和动态内容(如Java代码片段)混合在同一个文件中,以便更方便地创建动态Web页面。

主要区别和关系

  1. Servlet和JSP的关系

    • Servlet是Java编写的服务器端程序,通常用于处理客户端(浏览器)的请求,并生成响应。
    • JSP是在Servlet基础上发展而来的技术,它将Java代码嵌入HTML中,通过服务器在运行时将其转换为Servlet,然后执行。
  2. Servlet的特点

    • Servlet的应用逻辑通常在一个独立的Java类文件中编写,完全分离于HTML表示层。
    • Servlet处理HTTP请求和响应的全部细节,因此通常更适合处理业务逻辑和复杂的请求处理。
  3. JSP的特点

    • JSP允许开发者在一个扩展名为.jsp的文件中编写Java代码和HTML标记,使得页面的开发更加直观和快速。
    • JSP在服务器运行时会被转换成Servlet,并且和Servlet一样,能够与Java类和其他Java EE技术集成。
  4. 共同点和补充

    • Servlet和JSP都是Java EE(Enterprise Edition)规范的一部分,用于构建Web应用程序。
    • Servlet和JSP通常是结合使用的,例如Servlet处理业务逻辑和控制流程,而JSP负责呈现数据和页面的外观。

Servlet和JSP是Java Web开发中重要的两种技术,它们相辅相成,各有其独特的用途和优势。Servlet更注重于请求处理和业务逻辑,而JSP则更方便于页面设计和展示。它们共同构成了Java Web应用程序开发的基础框架,使得开发者能够以Java语言编写强大的、动态的Web应用程序。

4.Cookie和Session的区别

  1. 存储位置:Session 在服务器端,Cookie 在客户端(浏览器)。
  2. 默认存储位置:Session 默认存储在服务器的文件系统中,Cookie 存储在浏览器中。
  3. 依赖关系:Session 依赖于 session id,session id 通常存在于 Cookie 中。如果浏览器禁用 Cookie,可以通过其他方式传递 session id。
  4. 存储内容:Session 可以存储在文件、数据库或内存中,Cookie 只能存储在客户端,大小有限。
  5. 典型用途:Session 用于用户身份验证等需要保留用户状态的信息,Cookie 用于存储用户偏好等少量不敏感的数据。

5.JDBC访问数据库的基本步骤是什么?

  1. 加载驱动。
  2. 通过DriverManager对象获取连接对象Connection。
  3. 通过连接对象获取会话。
  4. 通过会话进行数据的增删改查,封装对象。
  5. 关闭资源。

6.说说preparedStatement和Statement的区别

  • PreparedStatementStatement 的子类,提供了更安全、更高效的 SQL 查询和更新操作方式。
  • PreparedStatement 通过预编译 SQL 语句和参数化查询来防止 SQL 注入攻击,提高了执行效率和代码的可维护性。

因此,通常情况下推荐使用 PreparedStatement 来执行 SQL 查询和更新操作,特别是涉及到动态生成 SQL 语句或需要多次执行相同结构的 SQL 语句时。

补充:

使用 Statement:

Statement statement = connection.createStatement();
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
ResultSet resultSet = statement.executeQuery(sql);

使用 PreparedStatement:

String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, username);
ResultSet resultSet = preparedStatement.executeQuery();

PreparedStatement 使用 ? 占位符来代替具体的参数值,通过 setXXX 方法设置参数值,从而避免了 SQL 注入攻击,并提高了执行效率和代码的可读性。

7.数据库连接池的原理,为什么要使用连接池。

  1. 数据库连接是一件费时的操作,连接池可以让多个操作共享一个连接。
  2. 数据库连接池的基本思想就是为数据库连接建立一个”缓冲池“。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需要从”缓冲池“中取出一个,使用完毕后放回。我们可以通过设置连接池最大连接数来防止系统无限的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量,使用情况,为系统开发,测试及性能调整提供依据。
  3. 使用连接池是为了提高对数据库连接资源的管理。

8.什么是事务?

事务就是被绑定在一起作为一个逻辑工作单元的SQL语句分组,如果任何一个语句操作失败,那么整个操作就会失败,之后就会回滚到操作前的状态。或者是上一个节点。为了确保要么执行,要么不执行,就可以使用事务。

补充:

事务关键特性ACID:

  • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,如果任何一部分操作失败,则整个事务都会被回滚(Rollback)到初始状态,以保证数据的一致性。

  • 一致性(Consistency):事务的执行使得数据库从一个一致性状态转变为另一个一致性状态。在事务开始之前和结束之后,数据库的完整性约束没有被破坏。

  • 隔离性(Isolation):事务的执行不受其他事务影响,即一个事务的修改在提交之前对其他事务是不可见的。这样可以防止多个事务并发执行时产生的一致性问题。

  • 持久性(Durability):一旦事务提交,其所做的修改就会永久保存在数据库中,即使系统发生故障,也不会丢失提交的数据。

9.drop,delete与truncate的区别

DROP用于删除整个数据库对象(表、索引等),且不可恢复。

DELETE用于逐行删除表中的数据,可以在事务中回滚。

TRUNCATE用于快速删除表中的所有数据,速度较快,但不可回滚且无法恢复被删除的数据。

10.在MyBatis中,#{}和${}的区别是什么?

在MyBatis中,#{}${}是两种常用的参数处理方式,它们在处理SQL语句中的参数时有所不同:

  1. #{}(预编译)

    • #{}是使用预编译的方式处理SQL语句中的参数,MyBatis会把SQL中的#{}替换为对应的参数值,并使用PreparedStatement中的占位符(?)来实现安全的SQL语句拼接,防止SQL注入攻击。

    • 示例:

      SELECT * FROM users WHERE id = #{userId}
      
    • #{}可以接收简单类型、JavaBean的属性或Map中的值作为参数,并且它会自动进行Java类型与数据库类型的转换。

  2. ${}(字符串替换)

    • ${}是进行字符串替换,MyBatis在处理SQL语句时会直接将${}替换为参数的值,不使用PreparedStatement,存在SQL注入的风险。

    • 示例:

      SELECT * FROM users WHERE id = ${userId}
      
    • ${}适合用于替换表名、列名等不需要预编译的场景,但是要注意参数的值必须是字符串类型,否则可能导致语法错误。

区别总结

  • #{}用于处理参数值,是预编译的方式,安全性高,可以防止SQL注入。
  • ${}用于替换SQL语句中的表名、列名等,直接进行字符串替换,存在SQL注入风险,不建议直接放置用户输入的参数。

在使用参数时,一般推荐优先使用#{},因为它能够提供更高的安全性和可维护性。

11.Mybatis支持延迟加载吗?如果支持,它的实现原理是什么?

  • MyBatis支持association关联对象(一对一关系)和collection关联集合对象(一对多关系)的延迟加载。也就是说,当你通过MyBatis查询一个对象时,可以选择延迟加载其关联的对象或集合。
  • 在MyBatis的配置文件中,可以通过设置lazyLoadingEnabled = true来启用延迟加载特性。这样配置后,当主对象查询时,关联对象或集合并不会立即加载,而是在第一次访问时才加载。

原理:

  • MyBatis使用动态代理技术(通常是基于CGLIB或Javassist)创建目标对象的代理。当你访问主对象的关联对象或集合时,代理拦截这些方法的调用。
  • 如果关联对象或集合尚未加载(即为null),MyBatis会发送另外的SQL查询来加载这些关联数据。
  • 加载完成后,会将加载得到的关联对象或集合设置回主对象,然后继续执行原始的方法调用。

比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用,这就是延迟加载的基本原理。

12.MyBatis中如何执行批处理?

使用BatchExecutor 完成批处理。执行update(没有select,JDBC批处理不支 持select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行 (executeBatch()),它缓存了多个Statement对象,每个Statement对象都是 addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

补充:

  • BatchExecutor 概述
    • BatchExecutor 是 MyBatis 提供的一个批处理执行器,用于在同一个批处理中执行多个更新操作。
    • 它缓存了多个 Statement 对象,每个 Statement 对象在 addBatch() 调用后,等待 executeBatch() 统一执行。
  • 具体操作示例
    • 在 MyBatis 配置中指定使用 BatchExecutor
    • 使用 SqlSession 提供的批处理功能来执行批量更新操作。
  • 批处理操作通常用于高效地执行大量的INSERT、UPDATE或DELETE语句,以提高性能和减少数据库连接次数。

13.查询语句不同元素(where,jion,limit,group by,having等等)执行先后顺序

  1. FROM:确定查询的数据源,包括表和视图。此阶段会处理表连接(JOIN)等操作。
  2. ON:在连接操作中,指定连接条件的过滤。
  3. JOIN:处理表连接(INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN 等)。
  4. WHERE:对从数据源中选取的行进行过滤,仅保留满足条件的行。
  5. GROUP BY:将结果集中的数据行分组,以便对每个分组进行聚合操作。
  6. HAVING:对分组后的结果进行过滤,仅保留满足条件的分组。HAVING 通常与聚合函数一起使用。
  7. SELECT:选取所需的列,并进行计算或别名的赋值。此阶段生成最终的结果集。
  8. DISTINCT:去除结果集中重复的行。
  9. ORDER BY:对结果集进行排序。
  10. LIMIT / OFFSET:限制返回的行数,并可指定从哪一行开始返回。

补充:

书写顺序SELECT -> FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> ORDER BY -> LIMIT

执行顺序FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT

四.多线程相关

1.进程与线程的区别,进程间如何通信?

进程:系统中资源分配的基本单位(强调在操作系统中的角色和作用),系统运行的基本单位(侧重在系统执行和调度方面的角色),进程之间彼此独立。线程之间运行可以相互影响。

线程:独立运行的最小单位(强调线程的执行和独立性)。操作系统能够进行运算调度的最小单位(强调线程在操作系统中的调度和执行角色)。一个进程包含多个线程,且它们共享同一进程内的系统资源,需要同步机制来避免资源竞争问题。(线程共享进程的资源,但每个线程有自己的栈、寄存器和程序计数器,能独立执行。)

进程间通过管道,共享内存,信号量机制,消息队列通信。

补充:

  • 进程
    • 资源分配:操作系统为每个进程分配必要的资源,如内存空间,CPU时间,文件句柄,网络连接等。
    • 独立性:每个进程有自己独立的地址空间,不同进程之间的资源和数据是相互独立的,除非通过特定的进程间通信机制。
    • 安全性和稳定性:由于进程间是隔离的,一个进程的错误或崩溃通常不会直接影响到其他进程,从而提高系统的安全性和稳定性。
    • 执行单元:进程是操作系统执行程序的基本实体。一个进程代表了一个正在运行的程序,包括程序代码和运行时的所有状态的信息。
    • 调度单元:操作系统的调度器负责管理进程的执行,将CPU时间分配给各个进程。调度器决定了哪些进程在什么时候运行,如何切换等。
    • 并发执行:通过多线程并发执行,操作系统可以在单个处理器上实现多任务,或者在多处理器上实现真正的并行执行。
  • 并发与并行(操作系统课程知识回顾(●’◡’●))
    • 并发(Concurrency)指的是在同一时间段内,多个任务在同一个处理器上交替执行。并发的重点在于任务之间的交替执行,以便在单个处理器上“同时”处理多个任务。并发编程通过时间分片和多任务切换,使得应用程序在一个处理器上看起来像是同时进行多个任务。适用于需要处理大量I/O操作或需要在单个处理器上处理多个任务的情况
    • 并行(Parallelism)指的是在同一时间点上,多个任务在多个处理器或多核处理器上同时执行。并行的重点在于真正的同时执行任务,并行编程通过分配不同的任务到不同的处理器核上,使得多个任务能够真正同时进行。适用于计算密集型任务,可以分割成多个独立子任务并行执行

2.什么是线程上下文切换

当一个线程被剥夺CPU使用权时,切换到另外一个线程执行。

补充:

  • 线程上下文切换 是指操作系统在多个线程之间切换执行时保存和恢复线程状态的过程。具体来说,当一个线程被暂停(例如因为时间片耗尽、被更高优先级的线程抢占、I/O操作、等待资源等),操作系统会保存该线程的上下文(包括寄存器、程序计数器、堆栈指针等),然后加载并恢复另一个线程的上下文,使其可以继续执行。这个过程称为 线程上下文切换

3.Synchronized和Lock的区别

  1. Synchronized是Java中的一个内置关键字,用于同步代码块或方法。Lock是一个接口,其常见实现类是ReentrantLock。
  2. Synchronize在发生异常时会自动释放锁,Lock需要手动调用unlock()方法来释放锁,如果在使用Lock时发生异常,必须在finally块中确保锁被释放。
  3. Synchronized是可重入锁,非公平锁,不可中断锁,Lock的ReentrantLock是可重入锁,可中断锁,可以是公平锁也可以是非公平锁。
  4. Synchronized是JVM层次通过监视器实现的,Lock是通过AQS实现的。
  5. 灵活性:Lock提供了更灵活的锁机制,允许尝试锁定(tryLock),尝试在特定时间内锁定(tryLock(long time, TimeUnit unit)),以及支持条件变量(Condition)。
  6. Lock在高并发下可以表现出更好的性能,因为它提供了更细粒度的控制和优化选项。Synchronized的性能在Java 6及以后版本中有了显著改进,尤其是在偏向锁和轻量级锁的引入后。
  7. Synchronized适合用于绝大多数简单的同步需求,因为其使用简单,语法更直观,且JVM会自动管理锁的释放。Lock适用于需要更高级控制和条件等待的场景,例如在复杂的多线程环境中,或者需要在等待锁时响应中断或超时。

五.常用开发框架系列

1.什么是Spring?

Spring是一个轻量级的框架,通过IOC达到松耦合的目的,通过AOP可以分离应用业务逻辑和系统服务进行内聚性的开发,不过配置各种组件时比较繁琐,所以后面才出现了SpringBoot框架。

2.IOC和AOP是什么?

IOC是控制反转,传统软件设计中,对象之间的关系由调用者创建被调用的实例,并且由调用者维护这个关系。而在控制反转中,创建和管理对象的权利转交给了容器或框架,这样的话,开发者只需要描述组件之间的关系,由框架负责实例化对象并自动注入到需要的地方。

AOP指的是面向切面编程,是将那些影响多个层次/模块的功能,从业务逻辑中分离出来的编程范式。这些功能被称为横向关注点。这样便于维护与单独定义,提高代码可重用性与可维护性。思想就是不侵入原有代码的情况下对功能进行加强。

补充:

  • DI:依赖注入(Dependency Injection,DI)是一种设计模式,它用于解耦组件之间的依赖关系。在Java中,Spring框架提供了一系列的注解来实现依赖注入。(AOP,IOC,DI是Spring的核心)

3.如何使用AOP自定义日志

  1. 创建一个切面类,把它添加到IOC容器中并添加@Aspect注解。
  2. 在切面类中写一个通知方法,在方法上添加通知注解并通过切入点表达式来表示要对哪些方法进行日志打印,然后方法参数为joinPoint。
  3. 通过joinPoint这个参数可以获取当前执行的方法名,方法参数等信息,这样就可以根据需求在方法进入或结算时打印日志。

补充:

AOP核心概念:

  • 切面(Aspect):切面是通知和切点的结合,它包含了一组通知和切点。切面定义了在哪里以及何时应该执行通知。比如,可能有一个日志切面,其中包含了在特定切点上执行的日志记录通知。
  • 切点(Point cut):切点就像是在道路上设置的路标或者标志,告诉你在哪些地点应该停下来做某些事情。比如设置了一个标志,表示在所有的红色路口停下来做记录。这个标志就是一个切点,它告诉你在哪些连接点上应用日志记录。
  • 连接点(Join Point):连接点就像是程序中的特定地点,比如方法调用、异常抛出等。想象一下程序是一条道路,连接点就是你能够停下来做一些事情的具体地点,比如路口、加油站等。
  • 通知(Advice):通知是在特定切点上执行的操作,它定义了在连接点上要执行的逻辑。通知是横切关注点的实际实现,可以在连接点之前、之后或者环绕执行。
  • 织入(Weaving):织入是把切面应用到目标对象上并创建代理对象的过程。这意味着在程序运行时,AOP框架将切面中的通知织入到应用程序的目标对象中,以实现横切关注点的功能。
// 切面
@Aspect
@Component
public class LoggingAspect {

    //  通知
//  execution(* org.example.service.*.*(..)) -> 切点的定义式,定义了匹配的连接点
    @Before("execution(* org.example.service.*.*(..))")
    public void logBefore() {
        System.out.println("Method execution is about to start.");
    }
}
  • 方法参数(比如JoinPoint)不是必须的,但它们可以提供更丰富的信息,比如JoinPoint.getSignature():返回当前被拦截的方法的签名信息。

4.SpringAOP中有哪些不同的通知类型

  1. 前置通知(@Before注解):在连接点之前执行的Advice。除非它抛出异常,否则没有能力中断执行流。
  2. 返回之后通知(@AfterReturning):在连接点正常结束之后执行的通知。如果一个方法没有抛出异常就正常返回。
  3. 抛出异常后执行通知(@AfterReturning):如果一个方法通过抛出异常来退出的话,这个通知就会被执行。
  4. 后置通知(@After):无论连接点是通过什么方式退出的(正常返回或者抛出异常)都会在结束后执行。
  5. 围绕通知(@Around):围绕连接点执行的通知。它可以在方法执行之前和之后执行自定义的行为,并且有能力中断执行流或改变方法的返回值。

补充:

  • 执行流(或控制流):在编程中指的是程序执行时指令的顺序和路径。

5.SpringAOP默认使用JDK动态代理还是CGLIB?

对于实现了接口的类,使用的JDK动态代理。对于没有实现接口的类,就使用CGLIB。

补充:

  • SpringAOP是基于动态代理实现的,一种是jdk动态代理,一种是cglib动态代理。、
    • jdk动态代理是利用反射来实现的,需要调用反射包下的Proxy类的newProxyInstance方法来返回代理对象,该方法有三个参数:类加载器(ClassLoader)、被代理类实现的接口(Class[])以及一个实现了InvocationHandler接口的对象。InvocationHandler接口的实现定义了代理对象的方法调用行为。
    • cglib动态代理原理是利用asm开源包来实现的,是把被代理类的class文件加载进来,通过修改它的字节码生成子类来处理。cglib动态代理不需要被代理类实现任何接口,它通过继承被代理类来创建代理对象。
  • jdk动态代理要求被代理类必须有实现的接口,生成的动态代理类会和代理类实现同样的接口。cglib则是,生成的动态代理类会继承被代理类。Spring默认使用jdk动态代理,当被代理的类没有实现接口时就使用cglib动态代理。

6.如何定义一个全局异常处理类?

想要定义一个全局异常类,需要在这个类上添加@ControllerAdvice注解,然后定义一些用于捕捉不同异常类型的方法,在这些方法上添加@ExceptionHandler(value = 异常类型.class)和@ResponseBody注解,方法参数是HttpServletRequest和异常类型,然后将异常消息进行处理。

如果需要自定义异常的话,就写一个自定义异常类,该类需要继承一个异常接口,类属性包括final类型的连续id,错误码,错误信息再根据需求写构造方法。

补充:

  • 在 Spring MVC 中,@ResponseBody 注解用于将方法的返回值转换为 JSON 格式(或其他格式,如 XML),然后通过 HTTP 响应发送给客户端。它通常用于 RESTful 服务中,将 Java 对象直接转换为客户端可以解析的格式。在全局异常处理中,如果希望将异常信息以 JSON 格式返回给客户端(比如前后端分离的 RESTful API 应用场景),通常会结合使用 @ExceptionHandler@ResponseBody 注解。
  • 在一个异常处理方法上添加 @ResponseBody 注解时,它的作用是告诉 Spring MVC,方法的返回值应该直接作为 HTTP 响应的主体内容发送给客户端,而不是将返回值视作视图名称或其他内容。

7.Spring中的常用注解(写出10个以上)

  1. 声明Bean的注解

    • @Component 将一个类标记为一个Spring组件。(通用的构造型注解,可以标注任何类型的类。)
    • @Service 标注业务逻辑层的类(用于Service层,表示服务(业务逻辑)组件)
    • @Repository 标注数据访问层的类(用于Dao(数据访问对象)层,表示数据库访问组件)
    • @Controller 标注展现层的类,控制器的声明(Controller层,用于Spring MVC 控制器类,处理HTTP请求并返回视图)
  2. 注入Bean的注解

    • @Autowired:由Spring提供,自动注入依赖项。
    • @Inject:由JSR-330(Java Dependency Injection)提供,与@Autowired类似。
    • @Resource:由JSR-250(Common Annotations for the Java Platform)提供,用于注入依赖项,默认按照名称进行注入,如果未找到匹配的名称则按照类型进行注入。
  3. Java配置类相关注解

    • @Configuration声明当前类为配置类,其内部组合了@Component注解,相当于XML形式的Spring配置。
    • @Bean注解在方法上,声明当前方法的返回值为一个Bean,@Bean 注解的方法可以代替XML中的 元素。
    • @ComponentScan 用于指定扫描哪些包中的组件。
  4. 切面(AOP)相关注解

    • @Aspect 声明一个切面,切面类中包含切点(PointCut)和通知(Advice)。使用@After,@Before,@Around定义通知。(通知的注解后跟切点参数,定义连接点)
    • @PointCut声明切点,在Java配置类中使用。(可复用在其他通知方法)
    • @EnableAspectAutoProxy注解开启Spring对Aspect的代理支持。
  5. Bean的属性支持

    • @Scope:设置Spring容器如何新建Bean实例。设置类型包括:
      • Singleton(单例,一个Spring容器中只有一个Beran实例,默认模式)
      • Prototype(每次调用新建一个Bean)
      • Request(web项目中,给每个http request新建一个Bean)
      • Session(web项目中,给每个http session 新建一个Bean)
      • GlobalSession(给每一个global http sessaion新建一个Bean实例)
    • @StepScope在Spring Batch中有涉及,用于声明批处理步骤范围的 Bean。
    • @PostConstruct 由JSR-250提供,在构造函数执行完后执行,等价于XML配置文件中的Bean的init-method
    • @PreDestory 由JSR-250提供,在Bean销毁之前执行,等价于XML配置文件中Bean的destory-method。
  6. @Value注解:为属性注入值。通常是从配置文件(如 application.properties 或 application.yml)中获取值。

  7. 环境切换

    • @Profile:通过设定Environment的ActiveProfiles来设定当前context需要使用的配置环境。
    • @Conditional:Spring4中可以使用此注解定义条件化的Bean,通过实现Condition接口,并重写matches方法,从而决定该Bean是否被实例化。
  8. 异步相关

    • @EnableAsync:配置类中,通过此注解开启对异步任务的支持。
    • @Async:在实际执行的Bean方法使用该注解来声明其是一个异步任务(需要@EnableAsync开启异步任务)
  9. 定时任务相关

    • @EnableScheduling:在配置类上使用,开启计划任务的支持。
    • @Schedule:声明这是一个任务,如 cron 表达式、固定延迟(fixedDelay)、固定速率(fixedRate)等。(需要先开启计划任务的支持)
  10. @Enable注解说明

    这些注解主要用来开启对 xxx 的支持。

    • @EnableAspeJAutoProxy:开启对AspectJ自动代理的支持。
    • @EnableAsync:开启异步方法的支持。
    • @EnableScheduling:开启计划任务的支持。
    • @EnableWebMvc:开启WebMVC的配置支持。
    • @EnableConfigrurationProperties:开启对@ConfigurtionProperties注解配置Bean的支持。
    • @EnableJpaRepositories:开启对Spring Data JPA Repository的支持。
    • @EnableTransactionManagement:开启注解式事务的支持。可以在 Spring 应用中使用 @Transactional 注解来声明事务。
    • @EnableCaching:开启注解式的缓存支持。可以在 Spring 应用中使用 @Cacheable@CachePut@CacheEvict 等注解来实现缓存功能。
  11. 测试相关注解

    • @RunWith 用于指定一个运行器,用于在测试类运行时控制测试的执行方式。在 Spring 中,通常使用 @RunWith 注解来对 JUnit 测试进行扩展,以便在测试中集成 Spring 的功能。

补充:

子包名称含义:

  • domain
    • 含义:表示领域模型包,通常包含与数据库表对应的实体类(Entity Classes)。
    • 用途:用来存放业务领域对象,例如 UserProduct 等与数据库表对应的类。
    • 生成的实体类会放在domain 包下。
  • pojo(Plain Old Java Object):
    • 含义:表示简单的 Java 对象包,通常包含没有业务逻辑的纯数据对象。
    • 用途:用来存放简单的 Java 对象,这些对象主要用于数据传输和存储。
    • 生成的简单数据对象会放在 pojo 包下
  • dao(Data Access Object):
    • 含义:表示数据访问对象包,通常包含与数据库操作相关的接口和实现类。
    • 示例com.example.project.dao
    • 用途:用来存放数据访问层代码,例如 Mapper 接口、数据库操作方法。
    • 生成的数据访问接口和类会放在dao 包下

类,对象,Bean的关系:

首先,在 Java 中,类是对象的模板或者蓝图,它描述了对象的属性和方法。当程序运行时,类的实例化过程就是创建对象的过程,通过关键字 new 可以实例化一个类,创建对象的实例。

而在 Spring 中,Bean 是 Spring IOC 容器中管理的对象,它们由容器负责创建、装配和管理。Spring IOC 容器负责实例化和管理 Bean,使得开发者无需手动创建对象,而是由容器负责创建和管理对象的生命周期。

在 Spring 中,我们使用 @Bean 注解或者在 @Component@Service@Repository 等注解标记的类来声明 Bean。这些注解标记的类在 Spring IoC 容器启动时会被扫描到,并且相应的 Bean 会被创建并加入容器管理。这样,这些被注解标记的类就变成了 Spring 容器中的 Bean,可以通过容器来获取和使用。

所以,虽然在 Java 中类是对象的模板,但在 Spring 中,我们可以使用注解将类声明为 Bean,并由 Spring IOC 容器来实例化和管理这些 Bean。这样,类就变成了 Spring 中管理的对象,也就是 Bean。(Bean 是一个由 Spring 容器管理的对象)

@Autowired
private UserService userService;

比如有一个类UserService使用了@Bean注解,我们不需要手动去实例化,使用@Autowired注解,Spring IOC 容器会自动将 UserServiceImpl 的实例注入到 userService 属性中。


8.Spring MVC的常用注解(写5个以上)

  • @EnableWebMvc在配置类中开启Web MVC的配置支持。

  • @Controller 声明该类为Spring MVC中的Controller。

  • @RequestMapping:用于映射Web请求,包括访问路径和参数。可以用在类或方法上,定义请求的 URL 映射。(@RequestMapping("/hello")

  • @RequestBody:允许将请求体中的数据绑定到方法的参数上,而不是直接连接在 URL 后面。

    • @PostMapping("/submit")
      public ResponseEntity<String> submitData(@RequestBody MyData data) {
          // 处理数据
          return ResponseEntity.ok("Data received");
      }
      
  • @ResponseBody:将方法返回值作为HTTP响应体返回,而不是一个视图,通常用于返回json数据。

  • @ParhVariable:用于接受路径参数,申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。

    • @GetMapping("/hello/{name}")
      public String hello(@PathVariable String name) {
          return "Hello, " + name;
      }
      
  • @RestController:该注解为组合注解,相当于@Controller和@ResponseBody。

  • @ControllerAdvice:通过该注解,可以将控制器的全局配置放置在同一个位置,,注解了@Controller的类的方法可使用@ExceptionHandler,@InitBinder,@ModelAtribute注解到方法上,这对所有注解了@RequestMapping的控制器内的方法有效。

  • @ExceptionHandler:用于全局处理控制器里的异常。

  • @InitBinder:用来设置WebDataBinder,WebDataBInder用来自动绑定前台请求到Model中。

  • @ModelAttribute:本来的作用是绑定键值对到Model里,在@ControllerAdvice中是让全局的@RequestMapping都能获得在此处设置的键值对。

9.Spring MVC的流程

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  4. DispatcherServlet调用HandlerAdapter处理器适配器。
  5. HandlerAdapter经过适配调用具体处理器(Handler,也叫后端控制器)。
  6. Handler执行完成返回ModelAndView(包含视图名和模型数据)。
  7. HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet。
  8. DispatchServlet将ModelAndView传给ViewResolver视图解析器进行解析。
  9. ViewResolver解析后返回具体View。
  10. DispatchServlet对View进行渲染视图(将模型数据填充至视图中)
  11. DispatcherServlet响应用户。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值