Featured image of post java集合

java集合

本文阅读量

集合

集合是一种容器,用来装数据的,类似于数组

集合大小可变,开发中常用

集合主要分为两类:CollectionMap

集合之Collection

单列集合,每个元素只有一个值

Collection是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的

Collection常用方法

 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
Collection<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写
// 1、添加数据
list.add("java");
list.add("python");
list.add("javascript");

// 2、清空集合
list.clear();

// 3、判断集合是否为空
System.out.println(list.isEmpty()); 

// 4、直接删除集合中的某个数据:默认只能删除第一个java
System.out.println(list.remove("java")); // true

// 5、判断集合中是否包含某个数据
System.out.println(list.contains("java")); // true

// 6、获取集合的大小(元素个数)
System.out.println(list.size()); // 3

// 7、把集合转化成数组
Object[] array = list.toArray();
System.out.println(Arrays.toString(array));

// 拓展
String[] arrays = list.toArray(String[]::new);
System.out.println(Arrays.toString(arrays));

// 8、把别人集合的数据加给自己
Collection<String> c1 = new ArrayList<>();
c1.add("java");
c1.add("python");
c1.add("javascript");


Collection<String> c2 = new ArrayList<>();
c2.add("php");
c2.add("golang");
// 把c2集合的数据全部导入给c1集合
c1.addAll(c2);

list

list系列集合特点:有序、可重复、有索引,list的常用实现类ArrayListLinkedList也具备这些特点。

这俩实现类的区别在于:底层采用的数据结构(存储、组织数据的方式)不同,适用场景不同

list集合因为支持索引,所以多了很多与索引相关的方法,当然了,Collection的功能List也都继承了

list特有功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 1、创建一个List集合对象
List<String> list = new ArrayList<>();
list.add("java");
list.add("python");
list.add("javascript");

// 2、给某个位置插入一个数据
list.add(1,"Go");

// 3、根据索引删除数据
System.out.println(list.remove(2));// 返回被删除数据

// 4、修改索引位置处的数据
list.set(2,"C++");// 将JavaScript修改为C++

// 5、根据索引取数据
System.out.println(list.get(2));

list实现类之ArrayList

ArrayList是基于数组实现的

创建ArrayList

1
ArrayList list = new ArrayList();

常见操作

 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
public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写

        // 添加数据
        list.add("java");
        list.add("python");
        list.add("javascript");
        System.out.println(list); // [java, python, javascript]

        // 2、插入数据
        list.add(1,"c++");
        System.out.println(list); // [java, c++, python, javascript]

        // 3、根据索引获取数据
        System.out.println(list.get(0)); // java

        // 4、获取集合大小(元素个数)
        System.out.println(list.size()); // 4

        // 5、根据索引删除数据,返回删除的数据
        System.out.println(list.remove(1)); // c++
        System.out.println(list); //[java, python, javascript]

        // 6、直接删除数据,返回真假
        System.out.println(list.remove("python")); // true
        System.out.println(list); // [java, javascript]

        // 7、修改某个索引位置处的数据,返回修改前数据
        System.out.println(list.set(1, "php")); //JavaScript
        System.out.println(list); // [java, php]

  			// 8、循环删除元素
  			// 8.1 方式1:每次删除一个数据后,索引-1
  			// 8.2 方式2:从集合后面遍历,然后删除数据,可以避免漏掉元素
    }

特点

  1. 查询速度快(是根据索引查询数据快):查询数据通过地址值和索引定位,查询任意数据耗时相同

    只需要获取起始索引地址,然后+要查询的索引值*单位长度,就获取到了要查询索引的地址,就可以获得相应的数据了

  2. 删除效率低:可能需要把后面很多的数据进行前移

  3. 添加效率极低:可能需要把后面很多的数据后移,再添加数据;同时也可能需要进行数组的扩容

底层原理

  1. 利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组(elementData)

  2. 添加第一个元素时,底层会创建一个新的长度为10的数组(size会记录当前集合的大小,同时也是下一个要添加数据的索引)

  3. 在我们要存第11个数据时,会扩容1.5倍(在底层重新创建一个1.5倍的新数组),把之前的数据复制进新数组,并添加第11个元素

    如果数组已经满了,我们批量添加数据(addAll())的长度+10>10*1.5,这个时候新创建的数组长度=批量添加数据(addAll())的长度+10

使用场景

**适合:**根据索引查询数据,如:根据随机索引取数据(高效)!或者数据量不是很大时!

**不适合:**数据量大的同时又要频繁的进行增删操作

list实现类之LinkedList

基于双链表实现的

单链表

链表中的结点(Node)是独立的对象,在内存中是不连续的,每个结点包含数据值和下一个结点的地址(手牵手)

我们把第一个结点称为头结点

结点有两部分组成下一个结点地址

单链表添加步骤

  1. 当我们要添加第一个数据到头结点时,会先将数据存储到头结点的结构中,然后会将第二个结点的地址存储到头结点的下一个结点地址结构中
  2. 当我们添加第二个数据时,数据会存储到第二个节点的结构中,然后会将第三个结点的地址存储到第二个结点的下一个结点地址结构中

单链表特点

  1. 查询慢,无论查询哪个数据都要从头开始找
  2. 链表增删相对快

双链表

结点(Node)有三部分组成前一个结点地址,下一个结点地址

头结点由^(头指针)下一个结点地址组成

尾结点由下一个结点地址^(尾指针)组成

典型的牺牲内存获取效率

特点

查询慢,增删相对较快,但对首尾元素进行增删改查的速度是极快的

linkedList特有方法

 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
// 因为对首尾元素进行增删改查的速度是极快的,所以新增了很多首尾操作的特有方法
// 使用LinkedList做队列
LinkedList<String> queue = new LinkedList<>();
// 入队
queue.addLast("第1个人");
queue.addLast("第2个人");
queue.addLast("第3个人");
queue.addLast("第4个人");

// 出队
System.out.println(queue.removeFirst()); // 第1个人
System.out.println(queue.removeFirst()); // 第2个人
System.out.println(queue.removeFirst()); // 第3个人
System.out.println(queue.removeFirst()); // 第4个人


// 使用LinkedList做栈
LinkedList<String> stack = new LinkedList<>();
// 入栈
stack.push("第1发子弹"); // push是对addFirst方法的包装
stack.addFirst("第2发子弹");
stack.addFirst("第3发子弹");
stack.addFirst("第4发子弹");

// 出栈
System.out.println(stack.pop());// 第4发子弹 pop是对removeFirst方法的包装
System.out.println(stack.removeFirst());// 第3发子弹
System.out.println(stack.removeFirst());// 第2发子弹
System.out.println(stack.removeFirst());// 第1发子弹

set

特点

无序(添加数据的顺序和获取出的数据顺序不一致)、不重复无索引

set要用到的常用方法,基本上就是Collection提供的,自己几乎没有额外新增一些常用功能

set的实现类之HashSet

hashSet是无序、不重复、无索引

基于哈希表实现的

**哈希值:**就是一个int类型的随机值,java中每个对象都有一个哈希值,java中的所有对象,都可以调用Object类提供的hashCode方法,返回该对象自己的哈希值

特点

  • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
  • 不同的对象,他们的哈希值一般不相同,但也有可能会相同(哈希碰撞)

**哈希表:**是一种增删改查数据,性能都较好的数据结构,在jdk8之前,哈希表=数组+链表,jdk8开始,哈希表=数组+链表+红黑树

Jdk8之前操作步骤

  1. 第一次添加数据时,创建一个默认长度为16的数组,默认加载因子是0.75,数组名table

  2. 使用原始的哈希值数组的长度做与运算(和求余类似),计算出应存入的位置

  3. 存入前,判断要存入位置是否为null,如果是null直接存入

  4. 如果不是null,表示已经存有元素,则调用equals方法与对应位置的链表内数据一一进行比较,如果有相等的,不存数据,没有相等,存入数组

    jdk8之前,新元素存入数组,占老元素位置,老元素挂新元素在下面,形成链表

    jdk8开始,新元素直接挂在老元素下面

加载因子作用:判断什么时候进行扩容。16*0.75 = 12,当存满12个元素后,就要扩容。

扩容成原来的2被,并开始迁移数据

jdk8开始

当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树

红黑树:左边放小的数据,右边放大的数据,基于二分查找来查询数据

HashSet集合去重

hashSet默认不能对内容一样的两个不同对象去重

 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
Set<Student> sets = new HashSet<>();
Student s1 = new Student("徐凤年",18,"游历");
Student s2 = new Student("青鸟",16,"丫环");
Student s3 = new Student("姜泥",18,"刺杀");
Student s4 = new Student("姜泥",18,"刺杀");
// 去重机制:
// 先拿到s3与s4的hash值(hashCode),算出要存储的位置,如果位置一致,使用equals判断内容是否一致

sets.add(s1);
sets.add(s2);
sets.add(s3);
sets.add(s4);

System.out.println(sets);// 我们发现,s3与s4的数据没有去重,因为s3与s4的哈希值是不同的
// 但是实际开发中,我们认为这两个数据是一样的,需要去重,这时候我们就要重写hashCode()与equals()方法了


// 重写equals()方法
@override
public boolean equals(Object o){
  if(this == o) return true;
  if(o == null || getClass() != o.getClass()) return false;
  Student student = (Student) o;
  return sex == student.sex && Objects.equals(name,student.name) && Objects.equals(hobby,student.hobby);
}
// 重写hashCode()方法
@Override
public int hashCode(){
  return Objects.hash(name,sex,hobby);
}
HashSet子类LinkedHashSet

特点

有序、不重复、无索引

底层原理

基于哈希表(数组、链表、红黑树)实现的,那它是怎么实现有序的?

LinkedHashSet每个元素都额外的多了一个双链表的机制记录它前后元素的位置(牺牲内存获取效率)基于LinkedHashMap实现

set的实现类之TreeSet

TreeSet是可排序(默认升序)、不重复、无索引

底层基于红黑树实现的排序

自定义排序规则

**方式1:**让自定义的类实现Comparable接口,重写里面的compareTo方法来指定比较规则

**方式2:**通过调用TreeSet集合有参数构造器,可以设置Comparator对象(比较器对象,用于指定比较规则)

注意

  • 对于数值类型:Integer,Double,默认按照数值本身的大小进行升序排序
  • 对于字符串类型:默认按照首字符的编号升序排序
  • 对于自定义类型,如Student对象,TreeSet默认是无法直接排序的

collection的遍历

for循环

因为list集合含有索引,所以支持for循序

1
2
3
4
5
6
7
8
9
// 1、创建一个List集合对象
List<String> list = new ArrayList<>();
list.add("java");
list.add("python");
list.add("javascript");
// 2、使用for循环遍历list集合
for(int i=0;i<list.size();i++){
  System.out.println(list.get(i));
}

迭代器

迭代器是用来遍历集合的专用方式(数组没有迭代器),在java中迭代器的代表是Iterator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ArrayList<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写
// 1、添加数据
list.add("java");
list.add("python");
list.add("javascript");

// 2、得到这个集合对象的迭代器对象(迭代器基于索引去取数据的)
Iterator<String> it = list.iterator(); // 迭代器处于第一个位置
System.out.println(it.next()); // 取第一个数据,然后游标指针下移一位
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next()); // 抛异常:NoSuchElementException
// 3、使用循环来获取数据
while(it.hasNext()){// hasNext()判断当前位置是否有元素存在
  System.out.println(it.next());// 当前位置有元素,获取当前位置的元素内容,然后在将游标指针下移一位
}

增强for

增强for既可以遍历集合也可以遍历数组

增强for遍历集合,本质就是迭代器遍历集合的简化写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Collection<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写
// 1、添加数据
list.add("java");
list.add("python");
list.add("javascript");

// 2、增强for循环遍历,快捷键:list.for+回车键
// 增强for只能读取数据,不能修改数据
for(String s:list){
  System.out.println(s);
}
int[] ages = {12,45,53,23};
for(int age: ages){
  System.out.println(age);
}

lambda表达式

Jdk8新增的方式,更简单、更直接来遍历集合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Collection<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写
// 1、添加数据
list.add("java");
list.add("python");
list.add("javascript");

// 使用对象回调思想来处理数据,本质使用增强for来对数据进行遍历
list.forEach(new Consumer<String>(){
  @Override
  public void accept(String s){
    System.out.println(s);
  }
});
// 使用Lambda简化
list.forEach(s->System.out.println(s));

list.forEach(System.out::println);

遍历并发修改异常

 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
Collection<String> list = new ArrayList<String>();  // jdk7之后,new后面的<>里内容可以不写
// 1、添加数据
list.add("java入门");
list.add("宁夏枸杞");
list.add("黑枸杞");
list.add("枸杞子");

// 1、使用迭代器遍历集合并删除含有枸杞的元素
// 注意:如果使用迭代器遍历,并用集合删除数据,会出现并发修改异常,程序出bug
Iterator<String> it = list.iterator();
while(it.hasNext()) {
  String name = it.next();
  if(name.contains("枸杞")){
    // list.remove(name);// 出现异常:ConcurrentModifiactionException   源码中:modCount 与 exceptedModCount不一致,每删除一个数据,modCount就会自加
    it.remove();// 必须调用迭代器自己的删除方法,才不会出现bug
  }
}
System.out.println(list);

// 2、使用增强for遍历集合并删除含有枸杞的元素,必然出错,而且无法解决
// 因为增强for的本质就是迭代器,而增强for无法拿到迭代器,因此无法解决

// 3、Lambda遍历集合并删除含有枸杞的元素,必然出错,而且无法解决

//4、如果是ArrayList带索引的集合,我们可以使用for循环删除后,再退一步(i--),或者从后面倒着百年路并删除

集合之Map

双列集合,每个元素包含两个值(键值对)

map集合成为双列集合(键值对集合),一次需要存入一对数据作为一个元素

set集合是基于map实现的

map格式为:{key1=val1,key2=val2,....},其中key=value称为一个键值对/键值对对象/一个Entry对象

特点

map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每个键只能找到自己对于的值

常用方法

 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
// 1、添加数据
Map<String, Integer> map = new HashMap<>();
map.put("华为手表",30);
map.put("Java入门",1);
map.put("iphone15",1);
map.put("雨伞",10);
map.put(null,null);
map.put("iphone15",2); // 后面重复的键会覆盖前面的整个数据


// 2、获取集合的大小(元素个数)
System.out.println(map.size());// 5

// 3、清空集合
map.clear();

// 4、判断集合是否为空
System.out.println(map.isEmpty());// true/false

// 5、根据键获取对应的值
System.out.println(map.get("雨伞")); // 10
System.out.println(map.get("手表")); // 没有这个键返回:null

// 6、根据键删除整个数据,返回删除数据对应的值。
System.out.println(map.remove("雨伞")); // 10,返回删除键对应的值

// 7、判断是否包含某个键
System.out.println(map.containsKey("雨伞")); // false
System.out.println(map.containsKey("iphone15")); // true

// 8、判断是否包含某个值
System.out.println(map.containsValue(1)); // true
System.out.println(map.containsValue(365)); // false

// 9、获取map集合的全部键,存储到一个Set集合中,然后返回
// public Set<K> keySet()
Set<String> keys = map.keySet();
System.out.println(keys);


// 10、获取map集合的全部值,存储到一个Collection集合中,然后返回
// public Collection<V> values()
Collection<Integer> values = map.values();
System.out.println(values);
for(Ingeger value:values){
 	System.out.println(value); 
}

遍历方式

 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
Map<String, Integer> map = new HashMap<>();
map.put("华为手表",30);
map.put("Java入门",1);
map.put("iphone15",1);
map.put("雨伞",10);
map.put(null,null);
map.put("iphone15",2); 

// 方式1:根据键查找对应的值
// 1、获取map集合的全部键
Set<String> keys = map.keySet();
// 2、根据键提取值
for(String key:keys){
  Integer value = map.get(key);
  System.out.println(key + "==>" + value);
}


// for(元素类型 变量:map){}  想通过增强for直接变量Map集合,但是无法遍历,因为键值对直接来看是不存在数据类型的
// 方式2:根据键值对进行遍历
// 将map集合转换成set集合,在转换的过程中,将map的键和值封装成一个Entry对象,放到set集合中
set<Map.Entry<String,Integer>> entries = map.entrySet();// 将map集合转换成set集合[(华为手表=30),(java入门=1)...]
for(Map.Entry<String,Integer> entry:entries){
  System.out.println(entry.getKey() + "==>" + entry.getValue);
}

// 方式3、Lambda表达式
map.forEach(new BiConsumer<String,Integer>(){
  @Override
  public void accept(String key,Integer value){
     System.out.println(key + "==>" + value);
  }
});
// 简化
map.forEach((key,value) -> {
  System.out.println(key + "==>" + value);
});

实现类

HashMap

是对Map集合接口的一种实现

特点

无序、不重复、无索引(这些特点是由键决定的)

底层原理

HashMap与HashSet的底层原理一样,都是基于哈希表实现的

Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import lombok.Data
  
@Data // Data自带了无参+get+set+toString+hashCode+equals
@NoArgsConstructor // 无参构造器
@AllArgsConstructor // 有参构造器,如果写了有参(Data中的无参就没有了)就必须写NoArgsConstructor,尽管Data中带了无参
public class Movie{
  private String name;
  private double score;
  private String actor;
} 

LinkedHashMap

是HashMap的子类

特点

有序、不重复、无索引(这些特点是由键决定的)

底层原理

底层数据结构依然是基于哈希表实现的,只是每个键值对元素中又额外的多了一个双链表的机制记录元素顺序(保证有序)

LinkedHashSet集合的底层原理就是LinkedHashMap

TreeMap

是对Map集合接口的另一种实现

特点

按照大小默认升序排序、不重复、无索引(这些特 点是由键决定的)

底层原理

TreeMap与TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序

排序

TreeMap集合同样也支持两种方式来指定排序规则

**方式1:**让类实现Comparable接口,重写比较规则

**方式2:**TreeMap集合有一个有参构造器,支持创建Comparator比较器对象,以便用来指定比较规则

集合嵌套

指的是集合中的元素又是一个集合(套娃)

1
2
3
4
5
Map<String,List<String>> privinces = new HashMpa<>();

List<String> cities1 = new ArrayList<>();
Collectioins.addAll(cities1,"南京市","扬州市","苏州市","无锡市");
privinces.put("江苏省",cities1);
使用 Hugo 构建
主题 StackJimmy 设计