文章目录
1. 数组与集合的区别?
在Java中,数组和集合都是容器,都可以用来存储多个数据,它们在使用的时候有一些区别:
1、类型:在Java中数组是最基本的数据结构,在内存中是线性存储的,可以用来存储基本数据类型(如int,double等)和引用类型(如对象)。集合(Collection),在Java中是一种接口,它是一种存储对象的容器,只能存储引用类型的数据。
// 数组
int[] arr1 = new int[5]; //int型数组
String[] arr2 = new String[5]; //对象数组
// 集合对象
List<String> list = ArrayList<>(); //存储Stirng类型数据,如果不指定泛型,它默认类型为object
2、长度:数组在初始化时需要指定长度(大小),一旦定义,其长度无法改变。而集合的长度是可变的,提供了add()、remove()等方法操作数据;(动态扩容、缩容)
int[] arr = new int[5]; //数组长度为5
3、操作数据:数组可以通过索引直接访问和修改元素,具有较高的访问效率,时间复杂度O(1)。而集合类通常提供了一系列方法来操作元素,例如添加、删除、查找等,对元素的访问需要通过方法调用。
// 1.数组通过索引方式操作数据
int[] arr = new int[5]; //int类似数组,默认值为0
// 添加数据
arr[1]=18;
// 覆盖数据
arr[1]=20;
// 访问数据
System.out.println(arr[1]);//20
// 获取数组长度
System.out.println(arr.length);
// 2.集合通过方法调用方式操作数据
List<String> list = ArrayList<>();
// 添加数据
list.add("张三");
list.add("李四");
list.add("王五");
// 删除数据
list.remove(2);
// 获取数据
System.out.println(list.get(1));//李四
// 获取集合中元素的个数
System.out.println(list.size());//2
4、内存分配方式:数组在内存中是连续存储的,占用的内存空间是固定的。而集合类中的元素可以在内存中不连续存储,可以根据需要动态分配内存空间。
5、功能扩展性:集合类提供了丰富的方法和功能,例如排序、查找、遍历等,可以方便地进行各种操作。而数组的功能相对较少,需要自己编写代码实现相应的功能。
6、泛型:集合支持泛型,可以指定存储的元素类型,提供了更好的类型安全性。(避免类型转换问题)
小结:
- 数组和集合都是容器,可以存储多个数据。
- 数组的长度是固定的,一旦创建,无法改变。
- 集合的长度是可变的,可以动态添加或删除元素。
- 数组可以存储基本类型数据和引用类型数据,而集合只能存储引用类型数据。如果要存储基本类型数据,需要使用对应的包装类。
- 数组可以通过索引方式直接访问元素,而集合通常通过方法调用进行元素的操作和访问。
总的来说,数组适用于长度固定且元素类型简单的情况,而集合适用于长度可变且元素类型复杂的情况。
单列集合:
双列集合:
2. List、Set、Map的区别?
List、Set和Map是Java中常用的集合接口,它们有以下区别:
-
List是一个有序的集合,可以包含重复元素。它允许按照插入顺序访问集合中的元素,并且可以根据索引位置进行操作。常见的实现类有ArrayList、LinkedList 和 Vector。
-
Set是一个不允许包含重复元素的集合。它不保证元素的顺序,即不按照插入顺序存储元素。常见的实现类有HashSet、TreeSet 和LinkedHashSet。
- HashSet:基于哈希表实现,不保证元素的顺序。
- TreeSet:基于红黑树实现,按照元素的自然顺序或指定的比较器进行排序。
- LinkedHashSet:基于哈希表和链表实现,保持元素的插入顺序。
-
Map是一种键值对的集合,每个键都是唯一的。它允许通过键来访问和操作对应的值。常见的实现类有HashMap、TreeMap和LinkedHashMap、ConcurrentHashMap。
-
HashMap:基于哈希表实现,不保证键值对的顺序。
-
TreeMap:基于红黑树实现,按照键的自然顺序或指定的比较器进行排序。
-
LinkedHashMap:基于哈希表和双向链表实现,保持键值对的插入顺序。
-
ConcurrentHashMap:线程安全的Map。
-
ConcurrentHashMap:
ConcurrentHashMap位于juc包,它是哈希表的线程安全版本,并且对HashMap进行改进。相比于HashMap,在多线程环境下,ConcurrentHashMap提供了更好的并发性能和线程安全性,主要通过以下几个方面来实现:
- 分段锁:ConcurrentHashMap将整个哈希表分成多个段(Segment),每个段维护着一个独立的哈希表。不同的线程可以同时访问不同的段,从而提高并发性能。每个段都有自己的锁,只有在修改该段的时候才需要加锁,这样可以减小锁的粒度,提高并发性能。
- CAS操作:ConcurrentHashMap使用了CAS(Compare and Swap)操作来保证线程安全。CAS是一种无锁的原子操作,它可以在不使用锁的情况下实现对共享变量的原子操作。
- volatile修饰符:ConcurrentHashMap使用volatile修饰符来保证可见性。volatile修饰的变量对所有线程可见,当一个线程修改了volatile变量的值,其他线程可以立即看到最新的值。
ConcurrentHashMap的使用方式与HashMap类似,可以通过put、get、remove等方法来操作元素。但需要注意的是,虽然ConcurrentHashMap是线程安全的,但在某些情况下,仍然需要额外的同步措施来保证一致性。
3. ArrayList、LinkedList、Vector的区别?
1、数据结构层面:
- ArrayList底层基于动态数组实现的,适合随机访问元素。
- LinkedList底层基于双向链表实现,适合数据的频繁插入和删除操作。
- Vector和ArrayList类似,底层也是基于动态数组实现的,Vector大部分方法都是线程同步的。(JDK1.0就诞生了)
2、线程安全层面:
-
ArrayList和LinkedList在多线程场景下都是不安全的(不提供内置的同步机制),需要外部同步(同步方法或者锁)。
- 如何保证ArrayList的线程安全:https://blog.csdn.net/qq_44544908/article/details/129020075
-
Vector是线程安全的,因为它的大部分方法都是同步的,但在性能上不如ArrayList和LinkedList。
3、性能层面:
- ArrayList在查询操作方面性能好,因为它底层基于动态数组实现,元素有索引,元素内存空间连续分配,查询操作的时间复杂度是O(1)。在不扩容的情况下,尾部添加元素的性能也很好,时间复杂度为O(1),而ArrayList在列表的中间或头部插入和删除元素时,需要进行移位操作,所以性能较差,时间复杂度是O(n),n是列表的长度。
- LinkedList在插入或删除操作性能好(双向链表),查询操作性能较差。
- Vector由于同步处理,性能较差,不推荐在非线程安全环境中使用。
示例:ArrayList、LinkedList、Vector的使用
package cn.z3inc.list;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 测试ArryList、LinkedList、Vector的使用
*
* @author 白豆五
* @version 2023/8/7
* @since JDK8
*/
public class ListDemo1 {
public static void main(String[] args) {
// ArrayList
List<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
System.out.println("arrayList = " + arrayList); // arrayList = [a, b, c, d, e, f, g]
arrayList.remove(0);
System.out.println("arrayList = " + arrayList); // arrayList = [b, c, d, e, f, g]
// LinkedList
List<String> linkedList = new ArrayList<>();
Collections.addAll(linkedList, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
System.out.println("linkedList = " + linkedList); // linkedList = [a, b, c, d, e, f, g]
linkedList.remove(0);
System.out.println("linkedList = " + arrayList); // arrayList = [b, c, d, e, f, g]
// Vector
List<String> vector = new Vector<>();
Collections.addAll(vector, "a", "b", "c", "d", "e", "f", "g"); // 批量添加
System.out.println("vector = " + vector); // vector = [a, b, c, d, e, f, g]
vector.remove(0);
System.out.println("vector = " + vector); // vector = [b, c, d, e, f, g]
}
}
运行结果:
小节:ArrayList底层基于动态数组实现,适用于随机访问遍历元素和尾部添加元素,但在中间插入和删除时性能较差;LinkedList底层基于双向链表实现,适用于插入和删除操作,但随机访问的性能较差;Vector与ArrayList类似但线程安全,性能较差,一般在多线程环境下使用。
4. Java集合的快速失败机制【fail-fast】
1、概念:
-
“fail-fast” 是Java集合类的一种错误检测机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如 某一个线程通过迭代器遍历集合时,如果其他线程对集合进行结构上的修改(添加、删除元素),则会抛出
ConcurrentModificationException
并发修改异常。 -
“fail-fast” 机制可以及时发现问题,避免后续操作基于错误的数据继续进行,但它并不能保证线程安全。因为它并不能阻止其他线程修改集合,只能在访问时进行检测。
2、原理:
-
“fail-fast” 机制的实现是通过记录集合在结构上被修改的次数来实现的。每个集合中都有一个
modCount
字段,用于记录集合的修改次数。每当对集合进行结构上的修改时,modCount
就会加1。而在进行迭代时,迭代器会存储一个expectedModCount
值,用于记录迭代器对集合进行修改的次数。 -
在迭代过程中,每次调用
hasNext()
和next()
方法时,会通过比较迭代器的expectedModCount和集合的modCount值来判断集合是否被修改过,如果两者不相等,则说明在迭代过程中有其他线程修改了集合,会立即抛出 ConcurrentModificationException异常。
示例:测试并发修改异常
package cn.z3inc.list;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* 测试并发修改异常
*
* @author 白豆五
* @version 2023/8/7
* @since JDK8
*/
public class ListDemo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 获取list的迭代器
Iterator<String> it = list.iterator();
// 遍历
//it.hasNext()判断是否有下一个元素
//it.next()获取元素并将指针向后移动一位
while (it.hasNext()) {
System.out.println(it.next());
list.add("f");
}
}
}
运行结果:
5. List接口常用方法
添加元素:
boolean add(E e)
:向列表尾部添加一个元素。add(int index, E element)
:向列表的指定位置处插入一个元素。addAll(int index,Collection<? extends E> c)
:把另一个集合中的所有元素按照指定位置插入到当前列表中。
获取元素:
-
E get(int index)
:获取列表中指定索引处的元素。 -
int indexOf(Object o)
:获取列表中指定元素第一次出现的索引。 -
int lastIndexOf(Object o)
:获取列表中指定元素最后一次出现的索引。
删除元素:
E remove(int index)
:从列表中删除指定索引处的元素,返回值是被删除的元素。boolean remove(Object obj)
:从列表中删除指定的元素(只删除列表中首次出现的元素)。
修改元素:
E set(int index,E element)
:修改列表中指定索引处的元素,返回值是被修改的元素。
其他方法:
int size()
:返回列表中的元素个数。boolean isEmpty()
:用于判断列表是否为空。boolean contains(Object o)
:用于判断列表是否包含指定的元素。void clear()
:清除列表中的所有元素。Object[] toArray()
:把列表转成数组Iterator<E> iterator()
:获取用于遍历列表的迭代器。
6. List的三种遍历方式
Collection集合支持两种遍历方式:
- Iterator迭代器。
- 增强for循环。
List接口继承了Collection接口,所以以上两种遍历方式也适用于List,除次以外,List集合还支持for循环+get(索引)方式遍历。
- Iterator迭代器。
- 增强for循环。
- for循环+get(索引)。
方式一: 使用迭代器(Iterator)进行遍历
迭代器是一种设计模式,它提供了一种统一的方式来遍历集合中的元素,而无需暴露底层数据结构的细节。通过使用迭代器,我们可以按顺序访问集合中的每个元素,迭代器模式封装了集合的内部结构,使得遍历过程更加简单、安全和灵活。
迭代器模式在Java中被广泛应用,例如在集合类(如List、Set)和Map类中都提供了迭代器来遍历元素。同时,我们也可以自定义实现迭代器接口来支持自定义的数据结构的遍历。
示例:
package cn.z3inc.list;
import org.junit.Test;
import java.util.*;
/**
* List集合3种遍历方式
*
* @author 白豆五
* @version 2023/8/7
* @since JDK8
*/
public class ListNBForDemo1 {
@Test
public void testIterator() {
// 1.创建list集合
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c");
// 2.获取list集合的迭代器对象
Iterator<String> it = list.iterator();
// 3.遍历
// hasNext() 判断是否有下一个元素
// next() 获取下一个元素
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
}
}
}
运行结果:
list.iterator()
:获取list集合的迭代器对象hasNext()
: 判断是否有下一个元素next()
: 获取下一个元素
方式二: 使用增强For进行遍历(foreach)
使用foreach遍历数组的底层实现是普通的for循环,而使用foreach遍历集合的底层实现是迭代器。
public class ListNBForDemo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c");
// 使用增强for遍历list集合
for (String item : list) {
System.out.println(item);
}
// 简写
//list.forEach(item -> System.out.println(item));
//list.forEach(System.out::println);
}
}
运行结果:
通过编译查看源码:javac -encoding utf-8 xxx.java
方式三:for循环+get(索引)方式遍历
public class ListNBForDemo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
7. ArrayList的扩容机制
在Java中,ArrayList 基于动态数组实现的,它可以根据需求自动扩容。当我们向ArrayList添加元素时,如果当前的容量不足以容纳新的元素时,ArrayList会自动进行扩容。
1、初始容量:
我们知道创建ArrayList对象时如果不指定长度,默认初始容量为10。
其实在创建一个空的ArrayList对象时,并没有立即初始化一个长度为10的数组,而是定义了一个空数组,直到我们第一次调用add
方法添加元素时,才会初始化一个长度为10 的数组。
所以,当我们创建一个空的ArrayList
对象时,并不会立即占用10个元素的空间,而是在真正需要存储元素时动态地进行扩容。
这种延迟初始化的方式可以节省内存空间,因为在创建ArrayList
对象时,并不总是需要预先分配一个固定大小的数组。只有在真正需要存储元素时,才会进行数组的初始化和扩容操作。(如果已经确定容量大小,并且容量大于10个,可以调用ArrayList的第二个构造器,避免频繁扩容)
2、扩容过程:
当ArrayList的元素个数超过当前容量时,就需要进行扩容操作。ArrayList的扩容操作是通过一个名为grow
的私有方法来实现的。
grow()方法首先计算新的容量大小,这个大小是旧容量的1.5倍(旧容量的一半再加上旧容量)。然后,通过Arrays.copyOf
方法创建一个新的数组,并将旧数组中的元素复制到新数组中。
// 扩容机制
private void grow(int minCapacity) {
// 老容量长度
int oldCapacity = elementData.length;
// 新容量长度 = 老容量长度+(老容量长度/2),右移一位相当于除以2
// 先扩容1.5倍,当新容量长度还是不够用,则直接使用所需要的长度minCapacity作为数组的长度。
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数组拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
-
扩容时机:是在add方法中通过调用
ensureCapacityInternal()
方法来触发的,而且只有当我们添加的元素个数超过了当前的容量时,扩容操作才会进行。 -
扩容性能:扩容操作会消耗一定的时间,需要频繁创建新数组,拷贝元素,销毁老数组。因此,如果我们事先知道要存储的元素个数,可以通过构造函数或者
ensureCapacity
方法来手动设置ArrayList的初始容量,避免频繁扩容。
ArrayList的扩容机制能够动态地适应数据量的变化,但是扩容操作会消耗一定的时间和资源。因此,在使用ArrayList时,我们应该尽量避免频繁的扩容,以提高性能和效率。
8. HashSet是如何保证数据唯一的?
在日常开发中,如果使用HashSet存储自定义对象,而且想要保证数据的唯一性,那么对象所属类必须重写equals()和hashCode()方法。
1、内部结构:
HashSet 底层基于 HashMap(也可以理解成哈希表) 来实现的,它实际上存储的是 HashMap 的 key。当我们把一个元素添加到 HashSet 中时,实际上是将这个元素作为 HashMap 的 key 存储,而 value 是一个固定的 Object 实例。
2、hashcode()方法:
当我们向 HashSet 添加一个元素时,首先会调用这个元素的 hashCode() 方法来计算哈希值。这个哈希值决定了元素在HashSet内部存放的位置。如果该位置上没有哈希值相同的元素,那么就将该元素存储到HashSet中;如果该位置上有哈希值相同的元素,就会产生哈希冲突(哈希碰撞),那么该位置会采用链表或红黑树来管理具有相同哈希值的元素。
3、equals()方法:
- 为了解决哈希冲突问题,会进一步调用元素的 equals() 方法和该位置上的所有元素进行比较。(比较对象是否相同)
- 只有当 hashCode() 相同且 equals() 返回 true 时,HashSet 才会认为这两个对象是相同的,并且不会重复存储。
4、为什么这样设计:
- 通过hashCode() 可以快速判断两个对象是否可能相同,因为计算哈希值相对较快,但它可能不精确。
- 通过equals() 方法可以精确判断两个对象是否真正相同,但它的执行可能会相对较慢。(因为它需要进行更多的比较操作)
这样设计可以在大多数情况下快速定位元素,并且只有在发生哈希碰撞时才需要执行equals()方法进行更精确的比较。
示例:测试HashSet数据唯一性
package cn.z3inc.set;
import java.util.HashSet;
import java.util.Objects;
/**
* 测试HashSet数据唯一性
* @author 白豆五
* @version 2023/8/10
* @since JDK8
*/
public class HashSetDemo01 {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
set.add(new Person("白豆五",18));
//即使是两个不同的对象,但它们的内容相同,HashSet也会认为它们是相同的,不进行存储
set.add(new Person("白豆五",18));
System.out.println(set);
}
}
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
// 重写equals方法
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 重写hashCode方法
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
注意事项:当我们重写 equals() 方法时,一定要重写 hashCode() 方法,以保持两者的契约。否则,当两个对象相等但哈希码不同时,HashSet 可能会存储两个相等的对象。
9. HashMap的实现原理?
1、HashMap的基本概念:
HashMap 底层是基于哈希表(也称散列表)来存储键值对的,不同JDK版本哈希表结构不太一样。HashMap能够在O(1)的时间复杂度下完成插入、删除和定位等操作。
HashMap继承关系:
HashMap的特点:
- 键具备哈希特性:哈希表,数组+链表/红黑树。
- 查询速度非常快、增删速度也不慢。
- 键要唯一:键所属的类要重写hashCode和equals方法,如果键重复了,旧值会被覆盖。
- 键无序:不能保证存入和取出的顺序是一致的。
- 键无索引:不能通过索引的方式获取键。
- 允许存储null键和null值。(在使用时需要避免空指针异常)
- 线程不同步,不安全,但效率高。
HashMap的常量:
/**
* 缺省 table数组初始大小为16, 2的4次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* table数组的最大容量,1073741824 (10亿)
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 缺省负载因子大小,0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化阈值8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树降级成为链表的阈值6
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树化的另一个参数,当table数组的长度大于64时,才会允许树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap的成员变量:
/**
* 哈希表(散列表)
*/
transient Node<K, V>[] table;
/**
* 存放所有键值对的集合
*/
transient Set<Map.Entry<K, V>> entrySet;
/**
* 哈希表中元素个数
*/
transient int size;
/**
* 当前哈希表结构的修改次数(用于实现fail-fast机制)
*/
transient int modCount;
/**
* 扩容阈值,当哈希表中元素个数大于threshold时,就会触发扩容(提高查找性能)
* threshold = capacity(数组长度) * loadFactor(0.75)
*/
int threshold;
/**
* 负载因子
*/
float loadFactor;
HashMap的构造方法:
/**
* HashMap的有参构造方法
*
* @param initialCapacity 初始容量大小
* @param loadFactor 负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
// 这3个if判断都是校验参数的合法性
// 如果初始容量小于0,则抛出异常
if (initialCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
}
// 如果初始容量大于最大容量(10个亿那个数),则将初始容量设置为最大容量
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY; // 限制数组最大长度为10个亿那个数
}
// 如果负载因子小于等于0或者负载因子是NaN ,则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
}
// 给负载因子、初始容量赋值
this.loadFactor = loadFactor;
// 返回一个扩容阈值,扩容阈值一定是2的次方数(如16、 32、 64、 128)
this.threshold = tableSizeFor(initialCapacity);
}
/**
* HashMap的有参构造方法
*
* @param initialCapacity 初始容量大小
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* HashMap的无参构造方法
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 给负载因子设置为0.75
}