集合类学习

数组与集合

相同之处:

  1. 它们都是容器,都能够容纳一组元素。

不同之处:

  1. 数组的大小是固定的,集合的大小是可变的。
  2. 数组可以存放基本数据类型,但集合只能存放对象。
  3. 数组存放的类型只能是一种,但集合可以有不同种类的元素。

集合根接口Collection

本接口中定义了全部的集合基本操作,我们可以在源码中看看。

img

我们再来看看List和Set以及Queue接口。

集合类的使用

List列表

首先介绍ArrayList,它的底层是用数组实现的,内部维护的是一个可改变大小的数组,也就是我们之前所说的线性表!跟我们之前自己写的ArrayList相比,它更加的规范,同时继承自List接口。

ArrayList

它的底层是用数组实现的,内部维护的是一个可改变大小的数组,也就是我们之前所说的线性表!跟我们之前自己写的ArrayList相比,它更加的规范,同时继承自List接口。

import java.util.ArrayList;
import java.util.List;

public class arrayList {
    public static void main(String[] args) {

        List<String> l = new ArrayList<>();   //ArrayList可以通过构造函数指定长度
        l.add("a");
        l.add("b");

        List<String> list = new ArrayList<>();   //ArrayList可以通过构造函数指定长度
        list.add("A");   //尾部插入
        list.add("B");
        list.add(1, "C");   //按位置插入
        System.out.println(list);
        System.out.println(list.contains("B"));   //判断是否有该元素   判断用的是equals方法,没有重写, 通过  == 判断
        System.out.println(list.containsAll(l));  //判断是否有参数List的里面的所有元素
        System.out.println(list.addAll(l));   //添加参数List的所有元素
        System.out.println(list);
        list.remove(list.size() - 1);   //按位删除
        System.out.println(list);
        list.removeAll(l);   //A.removeall(B)  删除a里面a与b的并集

        System.out.println(list);


        List<Student> s = new ArrayList<>();   //Student包含私有属性name和 有参构造
        s.add(new Student("A"));
        s.add(new Student("B"));
        System.out.println(s.contains(new Student("A")));    //false   所以我们需要改写Student的equals方法
        System.out.println(s.get(0).name);

    }
}
LinkedList

是一个双向链表 既实现了List结构也实现了Deque接口 所以LinkedList也能被当做一个队列或是栈来使用。

import java.util.*;

public class linkedList {
    public static void main(String[] args) {

        LinkedList<String> s = new LinkedList<>();
        s.add("A");  //尾部添加元素
        s.add("B");
        s.add(1, "C");  //指定位置添加元素
        System.out.println(s.element());
        s.remove();  //穿Index删除对应元素,不传值调用removeFirst()删除头元素
        System.out.println(s);
        s.offer("S");   //调用add()   add()调用linklast   在尾部添加元素   作为queue使用
        System.out.println(s.peek());   //查看头元素

    }
}
利用代码块来快速添加内容

前面我们学习了匿名内部类,我们就可以利用代码块,来快速生成一个自带元素的List

List<String> list = new LinkedList<String>(){{    //初始化时添加
    this.add("A");  this.add("B");
}};
System.out.println(list);   //[A,B]

集合的排序
        System.out.println(s);
        s.sort((o1, o2) -> {
            return o2.length() - o1.length();  //返回值为int   如果为负数交换位置    o1-o2 升序    02-01降序
        });
        System.out.println(s);

image-20220208152047114

迭代器 Iterator

集合的遍历

所有的集合类,都支持foreach循环!

        s.forEach(System.out::println);
        s.forEach(i-> System.out.println(i));
        s.forEach(i->{
            System.out.println(i);
        });
        for (String s1 : s) {
            System.out.println(s1);
        }
//结果都为
//C3333
//D4444
//A11
//B2

Iterable和Iterator接口

image-20220208155709698

反编译文件实际上是利用迭代器Iterator来对链表进行遍历

我们之前学习数据结构时,已经得知,不同的线性表实现,在获取元素时的效率也不同,因此我们需要一种更好地方式来统一不同数据结构的遍历。

由于ArrayList对于随机访问的速度更快,而LinkedList对于顺序访问的速度更快,因此在上述的传统for循环遍历操作中,ArrayList的效率更胜一筹,因此我们要使得LinkedList遍历效率提升,就需要采用顺序访问的方式进行遍历,如果没有迭代器帮助我们统一标准,那么我们在应对多种集合类型的时候,就需要对应编写不同的遍历算法,很显然这样会降低我们的开发效率,而迭代器的出现就帮助我们解决了这个问题。

我们先来看看迭代器里面方法:

public interface Iterator<E> {  //...}

每个集合类都有自己的迭代器,通过iterator()方法来获取:

Iterator<Integer> iterator = list.iterator();   //生成一个新的迭代器
while (iterator.hasNext()){    //判断是否还有下一个元素  
    Integer i = iterator.next();     //获取下一个元素(获取一个少一个) 
    System.out.println(i);
}

迭代器生成后,默认指向第一个元素,每次调用next()方法,都会将指针后移,当指针移动到最后一个元素之后,调hasNext()将会返回false,迭代器是一次性的,用完即止,如果需要再次使用,需要调用iterator()方法。

ListIterator<Integer> iterator = list.listIterator();   //List还有一个更好地迭代器实现ListIterator
import javax.swing.text.html.HTMLDocument;
import java.util.*;
import java.util.function.UnaryOperator;

public class linkedList {
    public static void main(String[] args) {

        LinkedList<String> s = new LinkedList<>();
        s.add("A11");
        s.add("B2");
        s.add(1, "C3333");
        s.add("D4444");

        ListIterator<String> listIterator = s.listIterator();
        Iterator<String> iterator = s.iterator();
        
        while (listIterator.hasNext()) {  //判断当前指针有没有后驱
            String next = listIterator.next();  //指针后移
            System.out.println(next);
        }                                        //指针移动了最后面
        System.out.println("=============================================");
        while (listIterator.hasPrevious()) {    //判断指针是否有前驱
            String previous = listIterator.previous();   //指针前移
            System.out.println(previous);

        }

    }
}

ListIterator是List中独有的迭代器,在原有迭代器基础上新增了一些额外的操作。

ListIterator有前驱的方法

image-20220208162924485

普通的Iterator没有

image-20220208163100999

Set集合

我们之前已经看过Set接口的定义了,我们发现接口中定义的方法都是Collection中直接继承的,因此,Set支持的功能其实也就和Collection中定义的差不多,只不过使用方法上稍有不同。

Set集合特点:

  • 不允许出现重复元素

  • 不支持随机访问(不允许通过下标访问)

HashSet底层是有HashMap实现

image-20220208205435198

利用HashMap的Key不可重复性来实现Set的不可重复性,只用Map的Key不用Value

image-20220208205758883

image-20220208205811908

LinkedHashSet 则是由 LinkedHashMap实现底层 (LinnkedHashMap可以存储顺序,HashMap存储这是无序的)

image-20220208210206418

image-20220208210227469

TreeSet 是有 TreeMap实现

image-20220208210941622

HashSet

首先认识一下HashSet,它的底层就是采用哈希表实现的(元素是链表的顺序表)

import java.util.*;

public class hashSet {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("D");
        set.add("A");
        set.add("C");
        set.add("B");
        set.add("A");    //set元素不能重复
        for (String s : set) {
            System.out.println(s);
        }
    }
}
//result
    A
    B
    C
    D

运行上面代码发现,最后Set集合中存在的元素顺序,并不是我们的插入顺序,这是因为HashSet底层是采用哈希表来实现的,实际的存放顺序是由Hash算法决定的。

那么我们希望数据按照我们插入的顺序进行保存该怎么办呢?我们可以使用LinkedHashSet:

import java.util.*;

public class hashSet {
    public static void main(String[] args) {
        HashSet<String> set = new LinkedHashSet<>();
        set.add("D");
        set.add("A");
        set.add("C");
        set.add("B");
        set.add("A");    //set元素不能重复
        for (String s : set) {
            System.out.println(s);
        }
    }
}
//result
	D
    A
    C
    B

LinkedHashSet底层维护的不再是一个HashMap,而是LinkedHashMap,它能够在插入数据时利用链表自动维护顺序,因此这样就能够保证我们插入顺序和最后的迭代顺序一致了。

LinkedHashSet

HashSet和LinkedHashSet使用比较

image-20220208210814935

TreeSet

还有一种Set叫做TreeSet,可以再创建数组是传入一个Comparable来实现控制排序,内部底层使用红黑树实现 , 它会在元素插入时进行排序:

image-20220208170703371

import java.util.*;

public class treeSet {
    public static void main(String[] args) {
        //TreeSet 底层是红黑树 
        TreeSet<Integer> set = new TreeSet<>((a,b) -> b-a);   //a-b升序  ,b-a降序
        set.add(3);
        set.add(1);
        set.add(4);
        set.add(2);
        System.out.println(set); //[4, 3, 2, 1]
    }
}

Map映射

什么是映射

我们在高中阶段其实已经学习过映射了,映射指两个元素的之间相互“对应”的关系,也就是说,我们的元素之间是两两对应的,是以键值对的形式存在。

映射

Map接口

Map就是为了实现这种数据结构而存在的,我们通过保存键值对的形式来存储映射关系。

HashMap和LinkedHashMap

HashMap的实现过程,相比List,就非常地复杂了,它并不是简简单单的表结构,而是利用哈希表存放映射关系,我们来看看HashMap是如何实现的,首先回顾我们之前学习的哈希表,它长这样:

img

哈希表的本质其实就是一个用于存放后续节点的头结点的数组,数组里面的每一个元素都是一个头结点(也可以说就是一个链表),当要新插入一个数据时,会先计算该数据的哈希值,找到数组下标,然后创建一个新的节点,添加到对应的链表后面。

而HashMap就是采用的这种方式,我们可以看到源码中同样定义了这样的一个结构:

image-20220208173043132

这个表会在第一次使用时初始化,同时在必要时进行扩容,并且它的大小永远是2的倍数!

/** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  

我们还发现HashMap源码中有这样一个变量,也就是负载因子,那么它是干嘛的呢?

负载因子其实就是用来衡量当前情况是否需要进行扩容的标准。我们可以看到默认的负载因子是**0.75**

image-20220208173224890

可以在创建HashMap时指定负载因子,不指定的话就是默认的

image-20220208173319747

那么负载因子是怎么控制扩容的呢?0.75的意思是,在插入新的结点后,如果当前数组的占用率达到75%则进行扩容。在扩容时,会将所有的数据,重新计算哈希值,得到一个新的下标,组成新的哈希表。

但是这样依然有一个问题,链表过长的情况还是有可能发生,所以,为了从根源上解决这个问题,在JDK1.8时,引入了红黑树这个数据结构。
img

当链表的长度达到8时,会自动将链表转换为红黑树,数组存放的就是红黑树的根节点,这样能使得原有的查询效率大幅度降低!当使用红黑树之后,我们就可以利用二分搜索的思想,快速地去寻找我们想要的结果,而不是像链表一样挨个去看。

/** * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn * extends Node) so can be used as extension of either regular or * linked node. */static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

除了Node以外,HashMap还有TreeNode,很明显这就是为了实现红黑树而设计的内部类。不过我们发现,TreeNode并不是直接继承Node,而是使用了LinkedHashMap中的Entry实现,它保存了前后节点的顺序(也就是我们的插入顺序)。

HashMap包含有 Node 和 TreeNode TreeNode 继承了 LinkedHashMap.Entry LinkedHashMap.Entry继承了HashMap.Node

img

HashMap的TreeNode讲解

(21条消息) HashMap之TreeNode_qq_40267688的博客-CSDN博客_treenode

/** * HashMap.Node subclass for normal LinkedHashMap entries. */
static class Entry<K,V> extends HashMap.Node<K,V> {  
    Entry<K,V> before, after;   
    Entry(int hash, K key, V value, Node<K,V> next) {   
        super(hash, key, value, next);}
}

LinkedHashMap是直接继承自HashMap,具有HashMap的全部性质,同时得益于每一个节点都是一个双向链表,保存了插入顺序,这样我们在遍历LinkedHashMap时,顺序就同我们的插入顺序一致。当然,也可以使用访问顺序,也就是说对于刚访问过的元素,会被排到最后一位。

import java.util.*;

public class Main {
    public static void main(String[] args) {
                                                                     //容量大小            负载因子        访问顺序,访问过得元素排在最后面
        LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put(1, "A");
        map.put(2, "B");
        map.put(3, "C");
        System.out.println(map);    //{1=A, 2=B, 3=C}
        map.get(2);
        System.out.println(map);    //{1=A, 3=C, 2=B}
        map.get(1);
        System.out.println(map);    //{3=C, 2=B, 1=A}

    }
}

观察结果,刚访问的结果被排到了最后一位。

TreeMap

TreeMap其实就是自动维护顺序的一种Map,就和我们前面提到的TreeSet一样:

    /**
     * The comparator used to maintain order in this tree map, or * null if it uses the natural ordering of its keys. * * @serial
     */
    private final Comparator<? super K> comparator;
    private transient Entry<K, V> root;

    /*** Node in the Tree.  Doubles as a means to pass key-value pairs back to* user (see Map.Entry).*/
    static final class Entry<K, V> implements Map.Entry<K, V> {

    }

我们发现它的内部直接维护了一个红黑树,就像它的名字一样,就是一个Tree,因为它默认就是有序的,所以说直接采用红黑树会更好。我们在创建时,直接给予一个比较规则即可。

Map的使用

import java.util.*;

public class Main {
    public static void main(String[] args) {
        //Map的Key不可以重复,Value可以重复 Map没有Iterator          设置排序条件,实现控制排序
        TreeMap<Integer, String> map = new TreeMap<Integer, String>((a, b) -> b - a) {{
            this.put(1, "LiuZhengWei");
            this.put(3, "JiangShengSen");   //内部类初始化
            this.put(2, "HuYaLi");
            this.put(0, "HuYaLi");
        }};
        System.out.println(map.getOrDefault(5, "没找到"));   //找的就返回对应的值,没找到返回设置的默认值
        System.out.println(map);   //Key 自动排序
        System.out.println(map.remove(5));  //移除对应键值对
        Set<Integer> integers = map.keySet();  //返回所有的Key   因为没有重复的所以用Set保存   Set不允许重复
        System.out.println(integers);
        Collection<String> values = map.values();//values() 默认返回的是Collection<V>   可以重复
        System.out.println(values);

        map.forEach((k, v) -> System.out.println(k + "->" + v));  //Map没有迭代器,但是1.8新增了forEach来遍历Map
        //下面两句也可以实现遍历,同上一句效果相同
        //for (Map.Entry<Integer, String> enrty : map.entrySet())
        //System.out.println(enrty.getKey() + "->" + enrty.getValue());
    }
}

image-20220208203315610

JDK1.8新增方法使用

再来看看JDK1.8中集合类新增的一些操作(之前没有提及的)首先来看看compute方法:

compute()

import java.util.*;

public class Main {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "刘正伟");
        map.put(2, "胡亚丽");
        map.put(3, "成龙");
        map.put(4, "奥利给");
        System.out.println(map.compute(5, (k, v) -> {      //为指定key重新计算值  如果没有该key  添加键值对
            return v + "alg";
        }));
        System.out.println(map.get(5));
        System.out.println("======================================================");

        System.out.println(map.computeIfPresent(10, (k, v) -> {  //如果该Key存在,修改Value    不存在return null
            return v + "Present";
        }));
        System.out.println(map.get(10));
        System.out.println("======================================================");

        System.out.println(map.computeIfAbsent(2, k -> {  //当不存在Key时,计算并将键值对放入Map   存在不执行,return该键的值
            return "该Key" + k + "不存在";
        }));
        System.out.println(map.get(2));
        System.out.println("Debug");
    }
}

image-20220208214348412

merge()

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("yoni", "English", 80),
                new Student("yoni", "Chiness", 98),
                new Student("yoni", "Math", 95),
                new Student("taohai.wang", "English", 50),
                new Student("taohai.wang", "Chiness", 72),
                new Student("taohai.wang", "Math", 41),
                new Student("Seely", "English", 88),
                new Student("Seely", "Chiness", 89),
                new Student("Seely", "Math", 92));
        Map<String, Integer> map = new HashMap<>();

        //遍历每一个student,将student的名字作为Key,Score作为Value,将Value求和
        students.forEach(student -> map.merge(student.getName(), student.getScore(), Integer::sum));
        map.forEach((k, v) -> {
            System.out.println("学生"+k+"的总分为: "+v);
        });

    }

    static class Student {
        private final String name;
        private final String type;
        private final int score;

        public Student(String name, String type, int score) {
            this.name = name;
            this.type = type;
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public int getScore() {
            return score;
        }

        public String getType() {
            return type;
        }
    }
}

image-20220208215633534
通过学习B站UP主 青空の霞光 讲的很细,感谢UP

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值