在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、泛型:集合支持泛型,可以指定存储的元素类型,提供了更好的类型安全性。(避免类型转换问题)
小结:
总的来说,数组适用于长度固定且元素类型简单的情况,而集合适用于长度可变且元素类型复杂的情况。
单列集合:
双列集合:
List、Set和Map是Java中常用的集合接口,它们有以下区别:
List是一个有序的集合,可以包含重复元素。它允许按照插入顺序访问集合中的元素,并且可以根据索引位置进行操作。常见的实现类有ArrayList、LinkedList 和 Vector。
Set是一个不允许包含重复元素的集合。它不保证元素的顺序,即不按照插入顺序存储元素。常见的实现类有HashSet、TreeSet 和LinkedHashSet。
Map是一种键值对的集合,每个键都是唯一的。它允许通过键来访问和操作对应的值。常见的实现类有HashMap、TreeMap和LinkedHashMap、ConcurrentHashMap。
HashMap:基于哈希表实现,不保证键值对的顺序。
TreeMap:基于红黑树实现,按照键的自然顺序或指定的比较器进行排序。
LinkedHashMap:基于哈希表和双向链表实现,保持键值对的插入顺序。
ConcurrentHashMap:线程安全的Map。
ConcurrentHashMap:
ConcurrentHashMap位于juc包,它是哈希表的线程安全版本,并且对HashMap进行改进。相比于HashMap,在多线程环境下,ConcurrentHashMap提供了更好的并发性能和线程安全性,主要通过以下几个方面来实现:
ConcurrentHashMap的使用方式与HashMap类似,可以通过put、get、remove等方法来操作元素。但需要注意的是,虽然ConcurrentHashMap是线程安全的,但在某些情况下,仍然需要额外的同步措施来保证一致性。
1、数据结构层面:
2、线程安全层面:
ArrayList和LinkedList在多线程场景下都是不安全的(不提供内置的同步机制),需要外部同步(同步方法或者锁)。
Vector是线程安全的,因为它的大部分方法都是同步的,但在性能上不如ArrayList和LinkedList。
3、性能层面:
示例: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类似但线程安全,性能较差,一般在多线程环境下使用。
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");
}
}
}
运行结果:
添加元素:
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 iterator()
:获取用于遍历列表的迭代器。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));
}
}
}
在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时,我们应该尽量避免频繁的扩容,以提高性能和效率。
在日常开发中,如果使用HashSet存储自定义对象,而且想要保证数据的唯一性,那么对象所属类必须重写equals()和hashCode()方法。
1、内部结构: HashSet 底层基于 HashMap(也就是 hashtable 哈希表) 来实现的,它实际上存储的是 HashMap 的 key。当我们把一个元素添加到 HashSet 中时,实际上是将这个元素作为 HashMap 的 key 存储,而 value 是一个固定的 Object 实例。
2、hashcode()方法:
当我们向 HashSet 添加一个元素时,首先会调用这个元素的 hashCode() 方法来计算哈希值。这个哈希值决定了元素在HashSet内部存放的位置。如果该位置上没有哈希值相同的元素,那么就将该元素存储到HashSet中;如果该位置上有哈希值相同的元素,就会产生哈希冲突(哈希碰撞),那么该位置会采用链表或红黑树来管理具有相同哈希值的元素。
3、equals()方法:
4、为什么这样设计:
这样设计可以在大多数情况下快速定位元素,并且只有在发生哈希碰撞时才需要执行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 可能会存储两个相等的对象。