一,Java基础
1. Java的三大特征?
- Java的三大特征为:封装,继承,多态。
- 封装:是指将对象内部的属性和实现细节隐藏起来,只暴露必要的供外部访问的接口。
-
- 好处:可以保护数据的安全,限制对数据的直接访问。提高代码的可维护性,可复用性和安全性。
- 继承:新创建一个类在现有类中派生出来,并继承了原有类的属性和方法。通过继承子类可以重用父类的代码,并在此基础上创建新的属性和方法。
-
- 好处:实现代码的复用和层次结构的建立。
- 多态:同一类型的对象在不同情况下表现的不同行为。分为重载和重写。
2. 抽象类和接口的区别?
- 定义方式:接口是只有方法签名而没有具体的实现,使用关键字"interface"来定义;抽象类可以包括抽象方法和具体方法的类,使用关键字"abstract" 来定义。
- 继承关系:一个类可以实现多个接口( 使用关键字"implements"),只可以继承一个抽象类( 使用关键字"extends")。
- 多态性:一个类可以实现多个不同的接口,从而表现出多态性,灵活地应对不同的需求;一个类只能继承一个抽象类,在继承关系中表现出单一继承。
- 目的性:接口主要用于定义类的行为和功能,强调"是什么";抽象类主要用于向下层继承提供共享的属性和方法,强调"是一个"。
3.java的反射机制原理?
反射机制是Java的一个重要特性,它允许程序在运行时获取自身的信息,并且可以动态的操作和修改类的行为。包括访问类的字段,方法,构造函数等信息,甚至可以在运行时创建对象,调用方法,以及修改字段的值。这样可以在编译时不知道类名的情况下,动态的操作类和对象。
-
举个例子来展示如何使用反射?
比如我们可以通过反射来动态创建一个对象。首先,我们需要获取目标类的class对象,使用class.forName(“类名”) 方法。然后通过Class对象的newInstance()方法,我们就能创建该类的实例。我们也能通过反射来动态的加载类,调用类中的方法和访问类中的属性。 -
反射的应用场景有哪些?
反射广泛用于很多框架和库中,比如Spring框架,ORM(对象关系映射)工具以及各种插件系统。还有像java的单元测试框架junit,他们就是利用反射来执行测试方法的。 -
反射的弊端有哪些?
反射在java中对封装性造成一定的破坏性,反射机制可以访问类的私有成员(私有字段,方法,构造函数等),这样可以绕过封装,直接访问本应该是私有的成员,导致封装性被破坏,这违背了封装的设计原则。反射还可以绕过泛型的类型检查,导致在运行时发生类型转换错误。在普通代码中,反射滥用还会导致代码可读性和维护性变差。 -
反射还需要注意哪些问题?
性能问题:反射比较慢相对于直接调用,因此性能要求较高的场景下,应该慎重使用。
安全问题:反射可以访问私有方法和属性,容易绕过访问和控制,所以在应用中应该慎重使用,避免出现安全隐患。
异常处理:由于反射是在运行时进行的,可能会抛出NoSuchMethodException,IllegalAccessException等异常,需要妥善处理。
4. java的异常
异常在java中是一种表示程序出现问题或错误的事件。它们通常在程序执行期间发生,并且可能会导致程序终止或产生意外结果。
- 例如:如果我们有一个数组,而我们试图访问数组的第100个元素,但实际上该数组只有50个元素,这时会抛给我们一个ArrayIndexOutOfBoundsException异常。这是因为我们试图访问一个不存在的数组索引。
- 异常分为哪几种类型
异常分为可检查异常和不可检查异常。
可检查异常是在编译时强制要求处理的异常,例如IOException;
不可检查异常是有运行时错误引起的,通常是程序员的错误,例如NullPointerException。
可检查异常:
IO异常: IOException
找不到类异常: ClassNotFoundException
文件找不到异常 :FileNotFoundExceptionn
Sql异常:SQLException
不可检查异常:
- 算数异常: ArithmeticException
- 数组下标越界异常 : ArrayIndexOutOfBoundsException
- 栈溢出异常:StackOverflowError
- 空指针异常: NullPointerException
- 非法参数异常: IllegalArgumentException
- 数据库连接异常:
- - SQLNonTransientConnectionException :表示无法连接到数据库的异常
- - SQLTimeoutException:表示数据库连接超时的异常
- - SQLInvalidAuthorizationSpecException:表示数据库连接授权验证失败的异常
- 如何处理异常?
Java中的异常处理通常使用try-catch块来捕获和处理异常。当程序执行到try块中的代码时,如果发生异常,则会跳转到catch块,执行catch块中的代码来处理异常。try { // 可能会发生异常的代码 } catch (ExceptionType1 e1) { // 处理异常类型1 } catch (ExceptionType2 e2) { // 处理异常类型2 } finally { // 无论是否发生异常,都会执行的代码 }
- 通过throws关键字在方法声明中抛出异常,从而将异常交给调用者处理。
public void readFile() throws FileNotFoundException { FileReader file = new FileReader("file.txt"); // 执行读取文件的操作 }
5. String类常用的一些方法有哪些?
- length():获取字符串的长度。
- equals(Object obj):比较字符串是否相等。
- isEmpty():判断字符串是否为空。
- toLowerCase():将字符串转换为小写。
- toUpperCase():将字符串转换为大写。
- trim():去除字符串两端的空白字符。
- substring(x,y):截取指定指定位置的子串。
6. String,stringBulider,stringbuffer的区别?
- 总结:
- String是不可变的,适用于频繁查询、少量修改的情况。
- StringBuilder是可变的,适用于单线程环境下的字符串拼接和修改。
- StringBuffer是可变的,线程安全的,适用于多线程环境下的字符串操作。
- StringBuffer线程安全的原理:
- StringBuffer的方法都使用synchronized关键字进行同步,这会带来一些性能上的损失 。
- string,stringBulider和stringbuffer的底层都是chart[]数组,为啥前者不可变,后者二者可变 ?
- 它们之间的可变性和不可变性是由设计决定的。
- String被设计为不可变对象,这意味着一旦创建了一个String对象,它的值就不能再被修改了。
- 为了实现这种不可变性,String采用了以下两种方式:
- 私有化char[]数组:在String类中,value字段(即存储字符串内容的char数组)被声明为私有的,并且没有提供公共方法来允许直接修改value。因此,任何对String对象的修改都必须通过创建新的String对象来完成。
- 缓存字符串常量:在Java中,字符串常量池是一个特殊的对象池,用于存储不需重复创建的字符串对象。因为字符串常量是在编译期间确定的,所以JVM可以将相同的字符串常量引用指向同一个对象。这样,多个String对象可以共享同一块内存。
- 为了实现这种不可变性,String采用了以下两种方式:
- StringBuilder和StringBuffer`都被设计为可变对象,允许对其进行原地修改。
- 为了实现这种可变性,它们采用了以下方式:
- 直接修改char[]数组:直接修改字符数组可以避免频繁地创建新对象,提高了效率。
- 动态扩展空间:在进行大量的字符串拼接时,可能会需要动态地扩展字符数组的长度。
- 为了实现这种可变性,它们采用了以下方式:
7. JDBC连接数据库的过程?
- 加载数据库驱动(到JVM)—>建立数据库连接 —> 创建数据库操作对象 —> 执行定义的SQL语句 —> 处理查询结果集 — > 释放资源(关闭对象)
8. Java重载和重写的区别
都是实现多态的两种方式,一种是编译时实现多态,一种是运行时实现多态。
重载:⼀个类中有多个同名的⽅法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者 ⼆者都不同)。
重写:发⽣在⼦类与⽗类之间,⼦类对⽗类的⽅法进⾏重写,参数都不能改变,返回值类型可以不相 同,但是必须是⽗类返回值的派⽣类。
9. 浅拷贝和深拷贝的区别?(非引用类型数据 和 引用类型数据)
- 浅拷贝只会复制对象本身和引用,不会复制对象引用指向的内容。
- 指的是创建一个新对象,
- 然后将原始对象的非引用类型成员变量的值复制到新对象中。
- 对于引用类型成员变量,浅拷贝只是简单地将引用复制到新对象中,使得新旧对象共享同一个引用。
- 换句话说,浅拷贝只复制对象的表面结构,不会递归复制对象的内部引用类型成员。
- 深拷贝会递归的复制对象和对象指向的内容,相对于创建一个全新的,与原对象完全独立的对象。
- 是在复制对象时,将原始对象及其所有引用类型成员都进行递归复制。
- 这样,在新创建的对象中,既有新的副本存储了所有的非引用类型成员变量的值,也有针对引用类型成员的全新对象,而不仅仅是引用的复制。
10. Java中throw和throws区别
- throws主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
- 说明你有那个可能,倾向。
- throw是具体向外抛异常的动作,所以它是抛出一个异常实例。
- 把那个倾向变成真实的了。
11. Java中常用的容器有哪些?
- 数组(Array)
- 集合框架(Collection Framework): 提供了一组接口和类,用于存储和操作一组对象。
- 列表(List):按照插入顺序存储元素,可以包含重复元素。
- 集合(Set):不允许重复元素的集合。
- 队列(Queue):按照特定规则进行插入和删除元素的集合。
- 映射(Map):使用键值对存储元素的集合。
- 迭代器(Iterator):用于遍历集合中的元素,提供了统一的遍历方式。
- 栈(Stack):后进先出(LIFO)的数据结构,常用于实现逆波兰表达式、深度优先搜索等算法。
- 队列(Queue):先进先出(FIFO)的数据结构,常用于任务调度、广度优先搜索等算法。
12. Java中HashSet的Value为什么存present而不是null?
- HashSet的add() 方法需要在加入新元素时返回 true,加入重复元素时返回 false
@Test void contextLoads() { // 创建一个新的 HashMap 对象 HashMap<String, Integer> hashMap = new HashMap<>(); // 使用 put() 方法向 HashMap 中添加键值对,并获取之前的值 Integer previousValue1 = hashMap.put("key1", 1); System.out.println("获取添加值的返回值: "+previousValue1); // 输出: null Integer previousValue2 = hashMap.put("key1", 2); System.out.println("获取添加值的返回值: "+ previousValue2); // 输出:1 // 输出 HashMap 的内容 System.out.println(hashMap); // 输出: {key1=2} }
- HashMap的put() 方法返回的是上一次以同一 key 加入的 value,若从未以该 key 加入任何数据,则返回 null
@Test void contextLoads2() { // 创建一个新的 HashSet 对象 HashSet<Integer> hashSet = new HashSet<>(); // 使用 add() 方法向 HashSet 中添加元素,并获取操作的结果 boolean added1 = hashSet.add(1); System.out.println("Added 1: " + added1); // true boolean added2 = hashSet.add(2); System.out.println("Added 2: " + added2); //true boolean added3 = hashSet.add(1); System.out.println("再次添加相同元素1: "+ added3); //false // 输出 HashSet 的内容 System.out.println(hashSet); // 输出: [1, 2] }
- 然而 HashMap 允许 null 作为 value所以如果使用 null 作为 value利用 HashMap
- 当返回 null 的时候我们就无法得知 null 究竟意味着这个 key 是第一次加入还是上一次使用了 null 作为 value 加入
13. ArrayList的扩容机制
- ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
- 扩容机制:
- 初始化:当我们创建一个 ArrayList 对象时,它会默认分配一个初始容量。默认情况下,初始容量为 10。
- 添加元素:当我们向 ArrayList 中添加元素时,如果当前的容量不足以容纳新的元素,就会触发扩容操作。
- 扩容操作:在进行扩容操作时,ArrayList 会创建一个新的数组,并将原始数组中的所有元素复制到新数组中。新数组的容量一般是当前容量的 1.5 倍(即增长约 50%),但如果指定了明确的容量增长规则,也可以根据规则进行扩容。
- 将新元素添加到扩容后的数组中:扩容完成后,ArrayList 就可以将新的元素添加到数组的末尾,同时更新数组的大小。
- 通过方法calculateCapacity(elementData, minCapacity)获取:
private static int calculateCapacity(Object\[\] elementData, int minCapacity) { //如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值 if (elementData == DEFAULTCAPACITY\_EMPTY\_ELEMENTDATA) { return Math.max(DEFAULT\_CAPACITY, minCapacity); } return minCapacity; }
- ensureExplicitCapacity方法可以判断是否需要扩容:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果最小需要空间比elementData的内存空间要大,则需要扩容 if (minCapacity - elementData.length > 0) //扩容 grow(minCapacity); }
- 接下来重点来了,ArrayList扩容的关键方法grow():
14. jre和jdk的区别?
- JRE 是用于运行 Java 程序的运行时环境。
- JDK 则是用于开发和编译 Java 程序的开发工具包。
- 如果你只需要运行 Java 程序,安装 JRE 就足够了。
- 如果你想进行 Java 程序的开发,那么你需要安装 JDK。
15. volatile的要点
- 什么是volatile:
- volatile 是一种关键字,用于声明变量
- volatile只能修饰成员变量,不能修饰局部变量,而且一般是和static配合使用
- 作用:
- 主要作用是保证变量在多线程环境下的可见性和禁止指令重排序。
- 可见性(JMM模型中主内存和线程内存)
- 当一个线程对 volatile 变量进行写操作后,其他线程立即可以看到最新的值,即保证了可见性。
- JVM会确保每次对 volatile 变量的写操作都立即刷新到主内存中,并且每次对 volatile 变量的读操作都从主内存中获取最新的值。这就保证了 volatile 变量的可见性。
- 顺序性(编译的时候,为提供性能,改变代码执行的顺序)
- 对一个volatile变量的读写具有原子性,且对于每个线程来说,其读写操作都是按照程序代码的顺序执行的。
- 也就是说,在一个线程内部,如果先写入了一个volatile变量,然后再读取它,那么写操作对于后续的读操作来说是可见的。
- 同样地,如果一个线程先读取了一个volatile变量,然后再写入它,那么读操作将看到之前的写操作的结果。
16. lock的底层是如何实现的?
- 什么是Lock?
- lock 是一个接口 。
- 主要的实现类:基于AQS(AbstractQueuedSynchronizer)实现的 。
- ReentrantLock:通过AQS的状态变量和等待队列来实现线程的同步和竞争。
- ReentrantReadWriteLock:
- ReentrantLock:
- 通过内部类 Sync 继承了 AQS,并重写了 AQS 中的一些方法,实现了可重入的独占锁。
- 它使用一个整型变量(state)来表示同步状态,
- 当 state 为0时,表示锁未被占用;
- 当 state 大于0时,表示锁已被占用,且持有锁的线程可重复获取锁;
- 当 state 小于0时,表示锁被占用,并且有其他线程在等待获取锁。
- 当一个线程尝试获取锁时,会调用 Sync 中的 tryAcquire(int acquires) 方法,该方法会根据锁的状态和当前线程的情况来判断是否可以获取锁。
- 如果可以获取锁,则将 state 值增加,并将其关联到当前线程,表示该线程持有锁。
- 如果不能获取锁,则当前线程会进入等待队列并阻塞,直到被唤醒并再次尝试获取锁。
- 当一个线程释放锁时,会调用 Sync 中的 tryRelease(int releases) 方法来释放锁资源。该方法会将 state 值减少,并根据情况决定是否唤醒等待队列中的其他线程。
- ReentrantLock 还提供了公平锁和非公平锁两种模式,通过指定构造函数中的参数来选择。公平锁会按照线程请求锁的顺序来分配锁资源,而非公平锁则允许插队获取锁资源,以提高并发性能。
- ReentrantReadWriteLock:
- 可重入的独占锁:是指一个线程在持有锁的情况下,可以多次获取同一个锁而不会发生死锁。 当一个线程已经获得了该锁时,它可以继续对该锁进行获取操作,而无需释放锁。
- 什么是CAS?
- 其他线程在获取资源的时候通过CAS的方式来防止并发的问题,同时也使得线程没有进入阻塞状态。
- CAS是一个自旋锁,在多个线程想修改主内存中的变量的时候,先将主内存的变量保存到线程本地内存中为预期值,然后将需要修改的值进行改成新值;然后将预期值和主内存的值比较,如果相同,那么则将主内存的值修改成新值,如果不相同,进入循环,再次将主内存的变量保存为目标值,再次修改新值,比较预期值和主内存的值。
17. synchronized的底层实现
- 对象头中,markoop存储monitor信息,记录哪个线程拥有锁
- 实现的过程:monitor开启监测,锁升级过程,monitor结束监测
- synchronized为了优化,具备升级策略,无锁,偏向锁,轻量级锁,重量级锁。
- 无锁状态:当一个线程访问同步代码块时,并没有其他线程争抢该资源,那么JVM会将该对象标记为无锁状态,也就是不会涉及到锁的申请和释放。
- 偏向锁状态:当只有一个线程访问同步代码块时,JVM会将该对象标记为偏向锁状态。
- 偏向锁是为了解决同一个线程多次获取锁的性能问题。
- 在偏向锁状态下,线程可以直接进入同步代码块,不需要进行加锁操作。
- 如果其他线程尝试竞争该锁,偏向锁会升级为轻量级锁状态。
- 轻量级锁状态:当多个线程同时访问同步代码块,但没有线程处于阻塞状态时,JVM会将该对象标记为轻量级锁状态。
- 轻量级锁使用CAS操作来实现锁的申请和释放,避免了线程的阻塞和唤醒过程。
- 重量级锁状态:当多个线程同时竞争同步代码块,并且有线程处于阻塞状态时,JVM会将该对象标记为重量级锁状态。
- 重量级锁使用操作系统的互斥量来实现锁的申请和释放,涉及线程的阻塞和唤醒。
18. jsp的内置对象?
- request:代表客户端的请求。可以使用它获取请求的参数、请求头信息等。
- response:代表服务器对客户端的响应。可以使用它设置响应头、输出响应内容等。
- session:代表用户的会话。可以使用它来存储和获取与特定用户相关的信息。
- application:代表整个 Web 应用程序的上下文。可以使用它来读取和设置全局的应用程序级别的属性。
- out:用于向响应中输出内容。
- pageContext:代表 JSP 页面的上下文。可以使用它来访问其他内置对象以及页面作用域中的属性。
- config:代表当前 JSP 页面的配置信息。可以使用它来读取 元素中定义的初始化参数。
- page:代表当前 JSP 页面本身。可以使用它调用 JSP 页面自身的方法。
19. java基础类库
- java.lang:提供了Java语言的核心类和基本工具类,如Object、String、Math等。它是Java程序中最基础、最常用的类库。
- java.util:提供了各种实用的数据结构(如集合、列表、队列、映射等)、日期时间处理、随机数生成、输入输出流等工具类。
- java.io:提供了读写数据流、文件操作、对象序列化等输入输出相关的类和接口,用于处理文件和网络数据的输入输出。
- java.math:提供了高精度的数值计算和大整数、大浮点数的运算类。
- java.sql:提供了访问关系型数据库的API,如连接数据库、执行SQL语句、处理结果集等。
20. 值传递和引用传递?
- 值传递:当将基本数据类型作为参数传递给方法时,实际上是将原始值的一个副本传递给方法。这意味着方法只能访问这个副本,并且对它的修改不会影响原始值。因为副本是独立的,所以在方法内部改变参数的值,不会影响到调用方。
- 引用传递:当将对象作为参数传递给方法时,实际上传递的是对象的引用(内存地址)。这意味着方法可以通过引用访问对象并修改其状态,这些修改会影响到原始对象。因为引用指向相同的对象,所以在方法内部修改对象的属性或调用对象的方法,会对调用方可见。
21. java的序列化
- 什么是序列化?
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
- 序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
- 序列化的应用场景:
- 对象在进行网络传输,传输之前–序列化,接收到之后–反序列化。
- 将对象存储到文件中,之前需要进行序列化 ,从文件中读取出来–反序列化。
- 将对象存储到数据库
- 将对象存储到内存
- 如果有些字段不想进行序列化怎么办?
- 可以使用 transient 关键字修饰。
- transient 修饰的变量值不会被持久化和恢复。
22. Java SPI 机制详解
- 为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。
- SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
- SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
23. SPI 和 API 有什么区别?
- 从广义上来说它们都属于接口 :
- 实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API 。
- 当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
24. jdk1.8新特性
- Lambda 表达式:Lambda 表达式是一种简洁的语法,用于表示匿名函数。它允许以更紧凑和清晰的方式编写代码,特别是在处理集合、函数式接口和并行计算时。
- 函数式接口:JDK 1.8引入了函数式接口,即只包含一个抽象方法的接口。它们可以与Lambda表达式一起使用,使得函数式编程更加方便。
- 默认方法(Default Methods):默认方法是指在接口中可以定义具有实现的方法。这样一来,在接口的所有实现类中都会自动继承该方法,从而避免了修改所有实现类的麻烦。
- 方法引用(Method References):方法引用提供了一种简洁的语法,用于直接引用已有的方法或构造函数。它可以进一步简化Lambda表达式的编写。
- Stream API:Stream API 是一种新的处理集合的方式,它提供了一种高效且易于阅读的方式来进行集合操作(如过滤、映射、排序等)。Stream API还支持并行计算,提高代码的性能。
- 新的日期/时间 API:JDK 1.8引入了全新的日期/时间 API,用于替代旧的Date和Calendar类。新的API提供了更简单、更安全和更易于使用的方式来处理日期、时间和时间间隔。
- 并发改进:JDK 1.8在并发编程方面进行了一些改进。
二,Java集合
1. List,set,Map的特点和使用场景?
-
List(列表):ArryList,LinkedList,Vector
- 特点:有序集合,可以包含重复元素。
- 使用场景:当需要按照特定顺序存储和访问元素时,或者需要允许重复元素存在时,可以使用List。
- 存储日志记录:日志记录通常需要按照时间顺序存储,因此可以使用List来存储日志信息。
- 存储用户操作历史记录:类似于日志记录,可以使用List来存储用户操作历史记录,以便进行回放等操作。
- 存储商品列表:在电商平台或者在线商店中,需要展示一系列商品,List可以用来存储商品信息。
- 存储排行榜:例如音乐榜单、股票涨幅榜等,List可以用来存储元素并根据指标排序,以便实现排行榜的功能。
-
Set(集合):HashSet,TreeSet,LinkedHashSet,EnumSet
- 特点:无序集合,不允许重复元素。
- 使用场景:当需要存储一组元素,但不允许重复元素存在时,可以使用Set。
- 存储账户名单:例如团队成员、会员名单等,Set可以用来存储唯一的账户名单,并且保证不重复。
- 存储用户喜好:例如用户喜爱的标签、类别等,Set可以用来存储这些元素,并且删除和添加都可以高效地完成。
- 存储投票选项:例如在线问卷、投票场景,Set可以用来存储每个选项,并且保证选项不重复,避免重复投票的情况发生。
-
Map(映射):HashMap,TreeMap,LinkedHashMap,ConcurrentHashMap 。
- 特点:键值对的集合,每个键都唯一,可以包含重复值。
- 使用场景:当需要通过唯一的键来访问和操作数据时,可以使用Map。
- 存储用户信息:例如用户ID、手机号码等可以用来作为键值对的键,用户信息可以作为值存储在Map中。
- 存储权限信息:例如角色、权限等可以用来作为键值对的键,具体的权限信息可以作为值存储在Map中。
- 存储配置文件信息:例如程序配置文件、数据库配置信息等可以用来作为键值对的键,具体的配置信息可以作为值存储在Map中。
2. List,set,Map的扩展
- List接口常见的扩展实现:
- ArrayList:基于数组实现的动态数组,支持快速随机访问元素,适合频繁读取数据的场景。它还提供了动态扩容机制,可以根据需要自动调整容量。
- LinkedList:基于链表实现的双向列表,支持快速插入和删除操作,适合频繁修改数据的场景。它的缺点是随机访问元素较慢,需要遍历链表。
- Vector:与ArrayList类似,是一个可扩展的动态数组,但是线程安全。由于它使用了同步机制,因此在多线程环境下使用Vector可能会导致性能下降。
- CopyOnWriteArrayList:与ArrayList类似,也是一个可扩展的动态数组,但是它是线程安全的。它通过在修改操作时进行复制,保证了读取操作的线程安全性。适用于读多写少的场景。
- Set接口常见的扩展实现:
- HashSet:基于哈希表实现的集合,不保证元素的顺序,允许存储空值(null),具有较快的插入和查找性能。**它使用hashCode和equals方法来判断元素的唯一性。
- TreeSet:基于红黑树实现的有序集合,按照元素的自然排序或者指定的比较器进行排序。它提供了一系列的有序操作方法,例如获取子集、范围查找等。
- LinkedHashSet:基于哈希表和链表实现的集合,可以按照元素插入的顺序迭代遍历。它在HashSet的基础上维护了元素的插入顺序,因此性能略低于HashSet,但迭代有更好的效率。
- EnumSet:专门用于存储枚举类型的集合,内部使用位向量实现,具有很高的性能和占用空间的效率。
- Map接口常见的扩展实现:
- HashMap:基于哈希表实现的键值对集合,不保证键值对的顺序,允许使用null作为键和值。它具有较快的插入和查找性能,适用于大多数情况。
- TreeMap:基于红黑树实现的有序键值对集合,按照键的自然排序或者指定的比较器进行排序。它提供了一系列的有序操作方法,例如获取子Map、范围查找等。
- LinkedHashMap:基于哈希表和链表实现的键值对集合,可以按照插入顺序或者访问顺序迭代遍历。它在HashMap的基础上维护了键值对的插入顺序或访问顺序,因此在迭代方面具有更好的效率。
- ConcurrentHashMap:在多线程环境下使用的哈希表实现的键值对集合,线程安全。它使用分段锁来提高并发性能,允许多个线程同时读取数据,而不需要加锁。
4. HashMap的底层原理?
- hashMap底层是由数组和链表实现的,jdk1.8之后加入了红黑树。
- 一个容量为16的数组,可存储key-value数据,同时记录负载因子和阈值。
- 当插入一个数据时,先根据key计算哈希码,再通过哈希码与当前数组长度进行取模运算,得到数据在数组中的索引位置。
- 若该位置为空(即无哈希碰撞),则直接将数据放入。
- 若该位置不为空(有哈希碰撞),此时会进行链表或红黑树的处理 :
- JDK1.8之前,使用的是链表来处理冲突。新插入的键值对会添加到链表的头部。
- 在JDK1.8及之后,当某个桶的链表长度大于阈值(默认8),并且数组容量大于阈值(默认为64),链表就会转换为红黑树,以提高查询效率 。
- 链表查询的时间复杂度为O(n),而红黑树查询的时间复杂度为O(log2n)
- 若节点长度小于6 则会转为链表。
- 总结:通过哈希算法将键映射到桶的位置,当发生哈希冲突时,使用链表或红黑树解决冲突。
- 扩容:
- 每次插入数据前,先检查前存储的数据数量是否达到了阈值。达到了触发扩容
- 创建一个新的两倍大小的桶数组,然后重新计算每个键值对在新数组中的位置。通过遍历旧的桶数组,将每个桶中的键值对重新分配到新的桶数组中。
5. HashMap用链地址法解决哈希冲突的时候有两种:头插法和尾插法,则他们的区别是?
- 头插法会造成一个死链的问题。
- 死链是指链表中存在无法通过遍历找到的节点。
- 尾插法的效率不如头插法,因为尾插法要遍历找到一个尾结点。
6,redis中的数据结构和HashMap类似,为啥他用的是头插法?
- redis是单线程的,所以不会有并发问题。
7,HashMap是如何解决数组下标冲突的?
- 一般采用链地址法 ,当多个键值对映射到相同的数组下标时,它们会被存储在同一个位置上,形成一个链表。
- 还可以开放寻址法:当发生哈希冲突时,就会探测散列表中的其他槽位,直到找到一个空闲的槽位来存储冲突的键值对。
- 常见的包括 线性探测、二次探测和双重哈希等。
- Threadlocal用到线性探测法。
8,concurrentHashMap的底层实现原理?
- 由多个Segment组成,每个Segment都是一个独立的哈希表。
- 每个Segment内部采用了与HashMap类似的哈希桶数组结构,可以存储多个键值对。
- 对其进行读操作不加锁,进行写操作加锁。避免并发冲突
- 在处理扩容时,只需要对某个Segment加锁,而其他Segment不受影响,减小了锁的粒度。
9,java的stream api 是迭代一次还是多次?
- filter是一个无状态的中间操作,对于这个中间操作来说,stream处理只需要迭代一次。但是对于有状态的中间操作,就需要迭代多次。
- 区分无状态和有状态的中间操作:
- 无状态的操作是指当前元素的操作不受前面元素的影响 。
- 有状态的操作是指需要等所有元素处理完之后才能执行当前操作
@Test void testContext(){ List<String> list= Arrays.asList("abb", "abcd", "fegc", "efe", "adfes"); System.out.println( list.stream().filter(s -> s.startsWith("a")).mapToInt(r -> length(r)).max().orElse(0));; } //stream() 方法将 list 转换成一个流对象,接着使用 //filter() 方法过滤出以字母 "a" 开头的字符串 //mapToInt() 方法把每个字符串转换为它的长度(即字符数),并返回一个 IntStream 对象。 //max() 方法获取 IntStream 中的最大值,并通过 orElse(0) 指定当 IntStream 为空时返回 0。
10,java中常用的容器?
- collection(集合):
- ListL: 必须按照插入的顺序保存元素 。自动扩容
- 大量的随机访问就要使用ArrayList 。
- 频繁在中间插入和删除就要使用LinkedList
- Set :不能有重复的元素 。
- HashSet提供最快的访问能力 。
- TreeSet保持元素排序状态 。
- LinkedHashSet以插入顺序保存元素。
- Queue :队列,先进先出。
- ListL: 必须按照插入的顺序保存元素 。自动扩容
- map(key-value):一组成对的值键对对象
- Iterator :迭代期
11,说一说ArrayList的实现原理
- ArrayList底层基于Object类型数组,实现了list接口。
- 初始时,数组的容量为10(可以通过构造方法指定初始容量),随着元素的添加,数组的容量会自动扩展。
- 当添加的数据超出数组长度时触发自动扩容,将旧数据拷贝到新数组中,新数组为就数组的1.5倍,
- 相对于同为继承list的LinkedList来说,查询快,增改删较慢,则LinkedList相反
12,ArrayList 和 LinkedList 的真正区别?
- ArrayList 维护的是一个动态数组,LinkedList维护的是一个双向链表
- 插入数据
- 一般情况,插入数据ArrayList要比LinkedList要快很多
- 两种情况下ArrayList慢
- 往集合中间插入数据时 ,ArrayList比linkedList慢
- ArrayList正好扩容的时候添加数据要比LinkedList慢
- ArrayList的扩容操作可能比LinkedList的扩容操作更耗时,因为ArrayList需要创建新的数组并复制元素。而LinkedList的扩容并不涉及改变数组大小,只需要调整链表结构
- 删除数据:
- AraayList要比LinkedList慢
- 查询数据:
- ArrayList比LinkedList快;
13,ArryList的扩容机制?
- ArrayList扩容发生在add()方法调用的时候 :
public boolean add(E e) { //扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData\[size++\] = e; return true; }
- ensureCapacityInternal() 扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
} - 通过方法calculateCapacity(elementData, minCapacity)获取:
private static int calculateCapacity(Object\[\] elementData, int minCapacity) { //如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值 if (elementData == DEFAULTCAPACITY\_EMPTY\_ELEMENTDATA) { return Math.max(DEFAULT\_CAPACITY, minCapacity); } return minCapacity; }
- ensureExplicitCapacity方法可以判断是否需要扩容:
private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果最小需要空间比elementData的内存空间要大,则需要扩容 if (minCapacity - elementData.length > 0) //扩容 grow(minCapacity); }
- ArrayList扩容的关键方法grow():
private void grow(int minCapacity) { // 获取到ArrayList中elementData数组的内存空间长度 int oldCapacity = elementData.length; // 扩容至原来的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组, // 不够就将数组长度设置为需要的长度 if (newCapacity - minCapacity < 0) newCapacity \= minCapacity; //若预设值大于默认的最大值检查是否溢出 if (newCapacity - MAX\_ARRAY\_SIZE > 0) newCapacity \= hugeCapacity(minCapacity); // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间 // 并将elementData的数据复制到新的内存空间 elementData = Arrays.copyOf(elementData, newCapacity); }
14,高并发中的集合有哪些问题?
- 线程安全性问题:
- 某些集合类(如ArrayList、HashMap)是非线程安全的,当多个线程同时对集合进行读写操作时,可能导致数据不一致或者出现并发冲突的情况。
- 解决:
- 这时可以使用线程安全的集合类(如Vector、ConcurrentHashMap)
- 使用同步机制(如锁、并发容器)来保证线程安全。
- 内存占用问题:
- 某些集合类在高并发情况下可能会占用大量的内存空间。
- 例如,HashMap在并发修改时可能导致很多链表节点形成长链,进而导致查询性能急剧下降。
- 解决:
- 可以考虑使用并发安全的集合类或者限制集合的大小,避免内存占用过高。
- 死锁问题:
- 当多个线程同时对不同的集合进行操作,且操作顺序不一致时,可能导致死锁的发生。
- 例如,线程A持有集合A的锁,并等待集合B的锁,而线程B持有集合B的锁,并等待集合A的锁。
- 解决:
- 避免产生不一致的加锁顺序,或者使用统一的加锁顺序来避免死锁。
- 性能问题:
- 某些集合类在高并发情况下可能会出现性能瓶颈。
- 例如,ConcurrentHashMap在高并发写入时可能会导致性能下降,因为它使用了分段锁来提高并发性能,在并发写入时需要竞争锁。
- 解决:
- 使用其他的并发容器或者进行优化,如使用无锁集合(如ConcurrentSkipListMap)或者分片操作。
- 迭代安全性问题:
- 部分集合类在迭代过程中进行修改会抛出ConcurrentModificationException异常。
- 例如,使用Iterator迭代ArrayList,如果在迭代过程中有其他线程对ArrayList进行修改,就会抛出此异常。
- 解决:
- 使用并发安全的集合类,或者采用CopyOnWriteArrayList等支持并发修改的集合类。
三,JVM
1,JDK 和 JRE 以及 JVM的区别?
- jdk是我们编写代码使用的开发工具包。
- jre是java的运行环境,大部分由c++和c编写的,提供编译java时所需要的基础类库。
- jvm:java虚拟机,java运行环境的一部分,虚构出来的一台计算机。
2,讲一下JVM内存结构?
- JVM内存结构分为5大区域:程序计数器、虚拟机栈、方法区【前三者线程私有】,本地方法栈、堆【后二者线程共享】。
- 程序计数器:用于记录当前虚拟机正在执行的字节码的行数。
- 作用:
- 实现对代码的流程控制
- 记录当前线程下次代码执行的位置
- 作用:
- 虚拟机栈:存放方法运行时的栈帧。
- 栈帧包括:局部变量表、操作数栈、动态链接、方法出口信息。
- 作用:用于存储局部变量、支持方法调用和返回、处理异常以及线程的生命周期管理。
- 本地方法栈:调用本地方法,提供 Native 方法服务。
- Native作用:与操作系统和硬件交互 ,融合现有代码库,提高性能。
- 堆:堆用于存放对象实例和数组,new出来的对象全部在堆中。
- 是垃圾收集器管理的主要区域,因此也被称作GC堆。
- 堆分为:新生代(Eden空间、From Survivor、To Survivor空间)和老年代。
- 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。
- 对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。
3,说一下堆栈的区别?
- 堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。
- 堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。
- 堆是线程共享的;栈是线程私有的。
4,什么情况下会发生栈溢出?
- 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常。这种情况通常是因为方法递归没终止条件。
- 新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出OutOfMemoryError异常。比如线程启动过多就会出现这种情况。
5,什么是类加载?类加载的过程?
-
Java 虚拟机负责把描述类的数据从 Class 文件 加载到 系统内存JVM 中,并对类的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称之为 Java 的类加载机制。
-
类的加载:指的是读取一个class文件,将其转化为某种静态数据结构存储在方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。
-
过程:
-
jvm类加载分三个阶段 :
- 加载:将jvm字节码文件加载到内存中,jvm使用双亲委派机制保护jvm内部的安全,以及防止类的重复加载 。
- 链接:链接又分为三个阶段
- 验证:主要验证加载的文件是否符合jvm规范 (文件格式验证在类加载的时候)
- 准备:为类变量赋予零值,类变量如果被final修饰则直接赋予定义的值
- 解析:将符号引用转化为直接引用。
- 初始化:为类变量赋值,为成员变量赋零值,执行静态代码块 。
7,什么是双亲委派模型?
- 一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
8,为什么需要双亲委派模型?
- 可以防止内存中出现多份同样的字节码。
- 通过父子关系的层级结构解决了类的隔离、安全性保护、代码复用和避免类重复加载等问题
9,什么是杀箱安全机制?
- 是一种用于隔离和限制代码执行环境的安全机制。它通过创建一个受限的执行环境,以防止恶意或不信任的代码对系统造成损害。
10,什么是类加载器,类加载器有哪些?
- 负责将类的字节码加载到内存并转换为可执行的Java类。 。
主要有一下四种类加载器:- 启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。
- 扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()获取它。
- 自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。
11,JVM的类加载器ClassLoader有什么作用?
- 加载类文件 :最主要的作用是将java类的字节码加载到内存中,并生成对应的class对象。
- 类的命名空间隔离:ClassLoader为每个加载的类创建一个独立的命名空间。
- 这意味着不同的ClassLoader加载的同一个类会被视为不同的类,从而实现类的隔离和版本控制。
- 解析和链接:ClassLoader在加载类的过程中会进行解析和链接操作。
- 解析阶段会将类的常量池、字段和方法的符号引用解析为直接引用。
- 链接阶段会进行验证、准备和解析等操作。
- 扩展机制和插件化:ClassLoader支持扩展机制,可以加载自定义的类。
- 这为应用程序提供了灵活的插件化和扩展性,允许动态加载和卸载类。
- 安全机制:ClassLoader可以实现安全策略,限制特定的类或代码只能由指定的ClassLoader进行加载。
- 通过ClassLoader的层次结构,可以实现类的访问控制和权限管理。
12,强引用,软引用,弱引用,虚引用的区别?
- GC是指垃圾回收器
13,JVM常见的垃圾回收器
- Serial收集器:Serial收集器是最古老、最简单的垃圾回收器,它以单线程方式进行垃圾回收,会触发全局暂停(stop-the-world)来执行垃圾回收操作。该收集器适用于较小的应用程序。
- Parallel收集器:Parallel收集器是Serial收集器的改进版,采用多线程进行垃圾回收,提高了回收效率。它同样会触发全局暂停来执行垃圾回收操作。适用于多核处理器的情况下,对于需要高吞吐量的应用程序比较合适。
- CMS收集器(Concurrent Mark Sweep):CMS收集器是一种并发垃圾回收器,它的特点是在垃圾回收过程中同时执行部分业务代码,减少了全局暂停时间。CMS收集器适用于对延迟敏感的应用程序,但它可能会牺牲一部分吞吐量。
- G1收集器(Garbage-First):G1收集器是一种面向服务端应用程序的垃圾回收器,它采用分代收集和并发标记整理算法,将堆内存分成多个区域(Region),根据垃圾情况进行有针对性的回收。G1收集器适用于大内存、低延迟的应用程序。
14,JVM垃圾回收算法?
- 标志清除法:
- 首先,通过根节点(如全局变量、活动线程等)开始遍历整个对象图,标记所有活动的对象。然后,清除未被标记的对象,并回收它们所占用的内存空间。
- 这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。
- 标志整理算法:
- 首先进行标记阶段,标记出所有活动对象。接着,将所有活动对象向一端移动,然后清理边界外所有的未被标记的内存区域。最后,更新指针并完成内存的压缩和整理。
- 复制算法:
- 将内存分为两个相等大小的区域,只使用其中一个。在进行垃圾回收时,先将存活的对象从一个区域复制到另一个区域,然后清空原来的区域,完成回收。这样可以避免内存碎片的产生。
- 特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。
- 分代收集算法 :
- 堆内存分为多个不同的代(Generation),通常是新生代(Young Generation)和老年代(Old Generation)。
- 新生代采用复制算法进行频繁的回收,
- 老年代采用标记-整理或标记-清除算法进行较少的回收。
- 堆内存分为多个不同的代(Generation),通常是新生代(Young Generation)和老年代(Old Generation)。
14,堆中存什么?栈中存什么?
- 堆中存的是对象。
- 栈中存的是基本数据类型和堆中对象的引用。
15, 内存的分配策略?
- 对象优先在 Eden 分配
- 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC。
- 大对象直接进入老年代 :大对象是指需要连续内存空间的对象 。
- 长期存活的对象进入老年代
- 动态对象年龄判定
- 空间分配担保
16,JVM性能调优监控工具
- JPS:jps主要用来输出JVM中运行的进程状态信息。
- JsTack: Jstack主要用来查看某个Java进程内的线程堆栈信息。
- Jmap和Jhat:jmap导出堆内存,然后使用jhat来进行分析 。
四,java的IO
1,什么是I/O流?
- Java的IO流是用于在程序和外部资源之间进行输入输出操作的工具。
- 它提供了一种统一的方式来处理不同类型的数据流。
- 在Java中,IO流主要分为字节流和字符流两种类型。
2,字节流
- 字节流以字节为单位进行读写操作,适合处理二进制数据(如图片、音频等)。
- InputStream(字节输入流):从源头(通常是文件)读取数据(字节信息)到内存中。
- 常见的字节流类有:
- InputStream:字节输入流的抽象基类,用于读取字节数据。
- OutputStream:字节输出流的抽象基类,用于写入字节数据。
- FileInputStream:文件字节输入流,可以从文件中读取字节数据。
- FileOutputStream:文件字节输出流,可以向文件中写入字节数据。
- BufferedInputStream/BufferedOutputStream:带有缓冲功能的字节流,提高IO的性能。
3,字符流
- 字符流以字符为单位进行读写操作,适合处理文本数据。
- 每个字符在内存中占用多个字节,因此字符流使用了编码解码的机制,将字节数据与字符数据进行转换。
- 常见的字符流类有:
- Reader:字符输入流的抽象基类,用于读取字符数据。
- Writer:字符输出流的抽象基类,用于写入字符数据。
- FileReader:文件字符输入流,可以从文件中读取字符数据。
- FileWriter:文件字符输出流,可以向文件中写入字符数据。
- BufferedReader/BufferedWriter:带有缓冲功能的字符流,提高IO的性能。
4,Java的IO流还包括一些其他特殊用途的流
- ObjectInputStream/ObjectOutputStream:用于对象的序列化和反序列化。
- DataInputStream/DataOutputStream:用于读写基本数据类型。
- PrintStream/PrintWriter:用于格式化输出文本数据。
- ByteArrayInputStream/ByteArrayOutputStream:用于在内存中读写字节数据。
5,不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型就很容易出现乱码问题。
6,Java IO 设计模式总结
- 装饰器模式: 可以在不改变原有对象的情况下拓展其功能。
- 通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
- 适配器模式: 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
- 适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。
- 适配器分为对象适配器和类适配器。对象适配器使用组合关系来实现,类适配器使用继承关系来实现,。
- 工厂模式
- 观察者模式
7,有哪些常见的 IO 模型?
- UNIX 系统下, IO 模型一共有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
- Java 中 3 种常见 IO 模型:
- BIO 属于同步阻塞 IO 模型 :
- 同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
- NIO属于同步非阻塞 IO 模型:
- AIO属于异步 IO 模型
- BIO 属于同步阻塞 IO 模型 :
8,NIO的作用?
- NIO 核心组件:
- Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer中,而写操作时将 Buffer中的数据写入到 Channel 中。
- Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过Channel来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
- Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到Selector上,由Selector来分配线程来处理事件。
·
9,如何解决大文件上传问题?
如果你的项目涉及到文件上传的话,面试官很可能会问你这个问题。
我们先看第一个场景:大文件上传中途,突然失败!
试想一个,你想上传一个5g的视频,上传进度到99%的时候,特么的,突然网络断了,这个时候,你发现自己竟然需要重新上传。我就问你抓狂不?
有没有解决办法呢?答案就是:分片上传!
- 什么是分片上传呢?
- 简单来说,我们只需要先将文件切分成多个文件分片〈就像我下面绘制的图片所展示的那样〉,然后再上传这些小的文件分片。
- 简单来说,我们只需要先将文件切分成多个文件分片〈就像我下面绘制的图片所展示的那样〉,然后再上传这些小的文件分片。
前端发送了所有文件分片之后,服务端再将这些文件分片进行合并即可。
- 使用分片上传主要有下面2点好处:
- 断点续传︰上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。所以,分片上传是断点续传的基础。
- 多线程上传︰我们可以通过多线程同时对一个文件的多个文件分片进行上传,这样的话就大大加快的文件上传的速度。
前端怎么生成文件分片呢?后端如何合并文件分片呢?
- 前端生成文件分片:
- 使用File API读取待上传的大文件。
- 将文件划分为固定大小的块,一般是几MB大小的块。
- 遍历每个文件块,利用FormData对象创建一个表单,将当前块作为表单的一部分,并将其上传到后台。
- 后端合并文件分片:
- 后端接收到上传的文件块后,根据自定义的标识或序号,按照顺序将这些文件块进行存储,可以将其暂存在服务器的临时文件夹中。
- 等到所有文件块都上传完毕后,根据预先定义的顺序,逐个读取这些文件块,并将它们按顺序合并成完整的文件。
- 合并完成后,将最终的文件保存到指定的目录中,并清理临时文件夹中的文件块。