Java学习-06 集合学习

Java学习-06 集合学习

1、类集

java对数据结构的成熟实现

Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。

所有的类集操作的接口或类都在 java.util 包中。

java类集结构图:

2、集合-常见的数据结构:

2.1、数组

数组:Array,是有序的元素序列。数组是一种连续存储线性结构,元素类型相同,大小相等

数组的优点:

  • 查找元素快:通过索引,可以快速访问指定位置的元素

数组的缺点:

  • 需要大块连续的内存块

  • 增删元素慢

    • 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原
      数组元素根据索引,复制到新数组对应索引的位置。
    • 指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应
      索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图

2.2、 链表:

链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

链表是离散存储线性结构。

n 个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。

链表优点:

  • 空间没有限制
  • 插入删除元素很快

链表缺点:

  • 查找速度很慢

链表分类:

  • 单链表:只有后续节点信息,没有前驱节点信息
  • 双向链表:既有后续节点信息,也有前驱节点信息
  • 循环链表:成环,即最后一个节点指向头结点

在这里插入图片描述

2、3、二叉树

二叉树:binary tree , 是每个结点不超过2的有序树(tree )。顶上的叫根结点,两边被称作“左子树”和“右子树”。

分类:

  • 斜树:所有结点都只有左子树,或者右子树。
  • 满二叉树:所有的分支节点都具有左右节点。
  • 完全二叉树:若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h
    层所有的结点都连续集中在最左边,这就是完全二叉树。

二叉树的一些性质:

  • 二叉树第 i 层上的结点数目最多为 2^(i-1) (i≥1)
  • 深度为 h 的二叉树至多有 2^h-1 个结点(h≥1)
  • 包含 n 个结点的二叉树的高度至少为 log2 (n+1)
  • 在任意一棵二叉树中,若终端结点的个数为 n0,度为 2 的结点数为 n2,则 n0=n2+1

二叉树的遍历方式:

二叉树的遍历方式,一般分为先序遍历,中序遍历,后序遍历。

  • 先序遍历
    o 先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
  • 中序遍历
    o 先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
  • 后序遍历
    o 先访问左节点,然后访问右节点,最后访问根节点(左->右->根)

2、4、栈

栈:stack, 又称堆栈,栈(stack)是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端

称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出的线性表 。

2、5、 队列

队列:queue, 简称队,队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表。队尾(rear)是允许插入的一端。队头(front)是允许删除的一端。空队列是不含元素的空表。先进先出

3、Collection

单值存取。

集合和数组既然都是容器,它们有啥区别呢?

  • 数组的长度是固定的。集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

集合按照其存储结构可以分为两大类,分别是单列集合 java.util.Collection 和双列集合java.util.Map。

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,

这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e) : 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e) : 把给定的对象在当前集合中删除。
  • public boolean contains(E e) : 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty() : 判断当前集合是否为空。
  • public int size() : 返回集合中元素的个数。
  • public Object[] toArray() : 把集合中的元素,存储到数组中。

List:允许重复值 ArrayList(95%)、Vector(4%)(线程安全)、LinkedList(1%)

List接口特点:

  1. 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  2. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  3. 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

Set:不允许重复值

3.1、List

List接口中常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

  • public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index) :返回集合中指定位置的元素。
  • public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
3.1.1、ArrayList

java.util.ArrayList 集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查

询数据、遍历数据,所以 ArrayList 是最常用的集合。

无参默认长度为10,传参数可以指定长度。视频中进行了源码分析:

  • 默认长度10是什么创建的?在第一次传递元素时扩容产生,因为默认创建时是长度为0.
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
  • add方法返回必然是true,最大长度为int类型的最大长度 - 8.
public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
  • 扩容算法是什么样子?在源码中。
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                 /**
                 * Default initial capacity.
                 */
                // private static final int DEFAULT_CAPACITY = 10;
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }
3.1.2、 Vector

它的扩容方式不同,可以传递增量大小,但如果小于0 ,那么就加上本身的长度。

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
3.1.3、LinkedList

java.util.LinkedList 集合数据存储的结构是链表结构方便元素添加、删除的集合。

使用的是双向链表结构,增删快,查找慢。可当成队列,栈使用。

LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可:

  • public void addFirst(E e) :将指定元素插入此列表的开头。
  • public void addLast(E e) :将指定元素添加到此列表的结尾。
  • public E getFirst() :返回此列表的第一个元素。
  • public E getLast() :返回此列表的最后一个元素。
  • public E removeFirst() :移除并返回此列表的第一个元素。
  • public E removeLast() :移除并返回此列表的最后一个元素。
  • public E pop() :从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e) :将元素推入此列表所表示的堆栈。
  • public boolean isEmpty() :如果列表不包含元素,则返回true

模拟栈:

压栈:push

弹栈:pop

模拟队列:

addLast

removeFirst

3.2、Set

不包含重复元素。

java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复

Set集合取出元素的方式可以采用:迭代器、增强for

3.2.1、HashSet

哈希集合,基于哈希表(散列表)。无序。

HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于: hashCode 与 equals 方法。

HashSet集合元素的唯一,是根据对象的hashCode和equals方法来决定的。所以如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

3.2.2、TreeSet

快速失败,安全失败,安全失败遍历的是复制的备份。有序的

使用这个存放自定义类时,需要实现Comparable 接口,以及他的 CompareTo方法,在里面定义规则。

规则要求:

  • 如果this 比 o 大,返回一个大于零的整数
  • 如果 this 比 o 小,返回一个小于零的整数
  • 相等,返回0.

不存储相等数据,就是借助了CompareTo方法,一样大,就不存。

演示:

package java03.com.app.core.section3;

import java.util.TreeSet;

/**
 * @Author: deemoHui
 * @Description:
 * @Date Created in 2020-08-04 17:12
 * @Modified By:
 */
public class TressSetDemo {
    public static void main(String[] args) {
        TreeSet<Person> data = new TreeSet<>();
        Person p1 = new Person("张三", 19);
        Person p2 = new Person("李四", 22);
        Person p3 = new Person("王五", 19);

        data.add(p1);
        data.add(p2);
        data.add(p3);

        for (Person p : data) {
            System.out.println(p);
        }

    }

    public static class Person implements Comparable<Person> {
        private final String name;
        private final int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public int compareTo(Person o) {
            if (this.age > o.age) {
                return 1;
            } else if (this.age < o.age) {
                return -1;
            }
            return 0;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }

    }
}

结果如下:

Person{name='张三', age=19}
Person{name='李四', age=22}

可以看到,王五并没有录进去,因为年龄和张三相等,被认为是相等的元素。

4、Collections

常用功能
java.utils.Collections 是集合工具类,用来对集合进行操作。部分方法如下:

  • public static boolean addAll(Collection c, T… elements) :往集合中添加一些元素。
  • public static void shuffle(List<?> list) 打乱顺序 :打乱集合顺序。
  • public static void sort(List list) :将集合中元素按照默认规则排序。
  • public static void sort(List list,Comparator<? super T> ) :将集合中元素按照指定规则排序。

5、Comparator比较器

说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用 java.lang.Comparable 接口去实现,一种是灵活的当我需要做排序的时候在去选择java.util.Comparator 接口完成。

那么我们采用的 public static void sort(List list) 这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:

public final class String implements java.io.Serializable, Comparable<String>,
CharSequence {

String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用

public static void sort(List list,Comparator<? super T> ) 方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:

public int compare(String o1, String o2) :比较其两个参数的顺序。

两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序

则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)

如果要按照降序排序

则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

简述Comparable和Comparator两个接口的区别。

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的
compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

6、Map

Mapping和Collection同一级别。存储一对数据,键值对。

基本方法:

6.1、哈希表

HashMap:哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
在这里插入图片描述

HashCode:根据这个值计算存储下标。元素比较多时,每个坑都是一个桶,可以存放多个值。17%16 = 1,33%16 =1, 一样的值存储到一个桶。

哈希桶中的数据量大于8时,会从链表转换为红黑树,减少到小于6时,会转换回去。
在这里插入图片描述

散列因子:当哈希表的桶0.75,即75%的桶都存储有数据了,那么就扩容一倍,即从16 - 32.

在这里插入图片描述

在这里插入图片描述

6.2、HashMap/Hashtable/ConcurrentHashMap:

多线程,线程安全与否。

HashMap:多线程时不安全,抢资源,效率高。

Hashtable:排队机制,效率低。

ConcurrentHashMap:分段锁机制,保证安全的前提提高效率。

在这里插入图片描述

TreeMap:无序存储,但是会对元素排序
LinkedHashMap:即保存在HashMap,也保存在双向链表,有序存储。

6.3、哈希值错乱问题:

如果自定义的类,当做哈希表的key存储在哈希表中,那么,不要去修改它,否则会引起哈希值错乱。如果确实有修改的需求,那么不要存在key上。

实例:

package java03.com.app.core.section3;

import java.util.HashMap;
import java.util.Objects;

/**
 * @Author: deemoHui
 * @Description:
 * @Date Created in 2020-08-04 19:54
 * @Modified By:
 */
public class HashMapDemo2 {
    public static void main(String[] args) {
        Person2 p1 = new Person2("mike", 18);
        Person2 p2 = new Person2("lucy", 20);

        HashMap<Person2, String> data = new HashMap<>();
        data.put(p1, "boy");
        data.put(p2, "girl");

        // 注意这一行,注释前和注释后,结果不同。就是是否修改key的影响
        p1.setName("tom");
        
        System.out.println(data.get(p1));

        Person2 p3 = new Person2("mike", 18);
        System.out.println(data.get(p3));
    }

    static class Person2{
        String name;
        int age;

        public Person2(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person2 person2 = (Person2) o;
            return age == person2.age &&
                    Objects.equals(name, person2.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, 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;
        }
    }
}

更改name之后,输出:

null
null

注释掉更改的那一句:

boy
boy

可以发现,更改key之后,会导致找不到原来的value,因为寻找是按照哈希值寻找,而哈希值已经不同。

6.4、Map 集合的输出

因为 Map 接口中存放的每一个内容都是一对值,而使用 Iterator 接口输出的时候,每次取出的都实际上是一个完整的对象。如果此时非要使用 Iterator 进行输出的话,则可以按照如下的步骤进行:
1、 使用 Map 接口中的 entrySet()方法将 Map 接口的全部内容变为 Set 集合
2、 可以使用 Set 接口中定义的 iterator()方法为 Iterator 接口进行实例化
3、 之后使用 Iterator 接口进行迭代输出,每一次的迭代都可以取得一个 Map.Entry 的实例
4、 通过 Map.Entry 进行 key 和 value 的分离

Map.Entry 本身是一个接口。此接口是定义在 Map 接口内部的,是 Map 的内部接口。此内部接口使用 static 进行定义,所以此接口将成为外部接口。

实际上来讲,对于每一个存放到 Map 集合中的 key 和 value 都是将其变为了 Map.Entry 并且将 Map.Entry 保存在了Map 集合之中。
在这里插入图片描述

在 Map.Entry 接口中以下的方法最为常用:

  • K getKey() 得到 key
  • V getValue() 得到 value
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class MapOutDemo01 {
        public static void main(String[] args) {
            Map<String, String> map = new HashMap<String, String>();
            map.put("ZS", "张三");
            map.put("LS", "李四");
            map.put("WW", "王五");
            map.put("ZL", "赵六");
            map.put("SQ", "孙七");
            Set<Map.Entry<String, String>> set = map.entrySet();// 变为Set实例
            Iterator<Map.Entry<String, String>> iter = set.iterator();
            while (iter.hasNext()) {
                Map.Entry<String, String> me = iter.next();
                System.out.println(me.getKey() + " --> " + me.getValue());
        }
	}
}

7、Iterator迭代器

在开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator 。Iterator 接口也是Java集合中的一员,但它与 Collection 、Map 接口有所不同,Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭代访问(即遍历)Collection 中的元素,因此 Iterator 对象也被称为迭代器

Iterator 属于迭代输出,基本的操作原理:是不断的判断是否有下一个元素,有的话,则直接输出。
在这里插入图片描述

通过 Collection 接口为其进行实例化之后,一定要记住,Iterator 中的操作指针是在第一条元素之上,当调用 next()方法的时候,获取当前指针指向的值并向下移动,使用 hasNext()可以检查序列中是否还有元素。

在这里插入图片描述

需要注意这个ListIterator 的add是在当前指针位置插入。

使用案例:

 Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }

8、forEach 增强for循环

最早出现在C#中。

用于迭代数组或集合(只能是Collection类下的数组集合)。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。forEach仅仅作为遍历操作出现。

语法:
for (数据类型 变量名:集合或数组名){}

9、JDK9的不可变集合

增加了一些确定长度的不可变集合,包括List,Set,Map。

示例:

package java03.com.app.core.section3;

import java.util.List;

/**
 * @Author: deemoHui
 * @Description:
 * @Date Created in 2020-08-04 20:08
 * @Modified By:
 */
public class JDK9Demo {
    public static void main(String[] args) {
        List<String> list = List.of("12345", "67890");
        for (String s :list) {
            System.out.println(s);
        }
        // 下面这行代码会出错
        list.add("haha");;
    }
}

运行结果:

12345
67890
Exception in thread "main" java.lang.UnsupportedOperationException

Set 和Map示例:

package java03.com.app.core.section3;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @Author: deemoHui
 * @Description:
 * @Date Created in 2020-08-04 20:08
 * @Modified By:
 */
public class JDK9Demo {
    public static void main(String[] args) {
        /*List<String> list = List.of("12345", "67890");
        for (String s :list) {
            System.out.println(s);
        }*/
        // list.add("haha");;

       /* Set<String> set = Set.of("abcde", "hijk");
        for (String s :
                set) {
            System.out.println(s);
        }*/

        Map<String, String> map = Map.of("111", "1", "222", "2");
        Set<String> keys = map.keySet();
        for (String key :
                keys) {
            System.out.println(key + "-->" + map.get(key));
        }
    }
}

10、java.lnag.Object 中对 hashCode 的约定

  1. 在一个应用程序执行期间,如果一个对象的 equals 方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode 方法多次,它必须始终如一地返回同一个整数。
  2. 如果两个对象根据 equals(Object o)方法是相等的,则调用这两个对象中任一对象的 hashCode 方法必须产生相同的整数结果。
  3. 如果两个对象根据 equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的 hashCode 方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。

在 java 的集合中,判断两个对象是否相等的规则是:

(1)判断两个对象的 hashCode 是否相等

  • 如果不相等,认为两个对象也不相等,完毕
  • 如果相等,转入 2

(这一点只是为了提高存储效率而要求的, 其实理论上没有也可以,但如果没有,实际使用时效率会大大降低 , 所以我们这里将其做为必需的。)

(2)判断两个对象用 equals 运算是否相等

  • 如果不相等,认为两个对象也不相等
  • 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)

当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而造成内存泄露

猜你喜欢

转载自blog.csdn.net/deemo_hui/article/details/108173895