zoukankan      html  css  js  c++  java
  • Java集合基础知识笔记:List集合(为什么需编写equals方法)、Map集合(equals与hashCode方法)、EnumMap好处、有顺序的TreeMap、读写配置文件Properties、Set集合(不重复的key值)与有顺序的Set、队列、优先队列、双端队列、栈、迭代器

      在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。引入集合的原因是方便处理一组类似的数据。

      数组有如下限制:(1)数组初始化后大小不可变;(2)数组只能按索引顺序存取。

      因此,我们需要各种不同类型的集合类来处理不同的数据,例如:(1)可变大小的顺序链表;(2)保证无重复元素的集合;等。

      Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括ListSetMap。Java集合使用统一的Iterator遍历,尽量不要使用遗留接口。

    一、List

    1、List是最基础的一种集合:它是一种有序列表。List的行为和数组几乎完全相同:List内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List的索引和数组一样,从0开始。

    2、数组的删除与添加元素:“删除”操作实际上是把后面的元素依次往前挪一个位置,而“添加”操作实际上是把指定位置以后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。这两种操作,用数组实现非常麻烦。

    3、在实际应用中,需要增删元素的有序列表,我们使用最多的是ArrayList。实际上,ArrayList在内部使用了数组来存储所有元素。例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位)。当添加一个元素并指定索引到ArrayList时,ArrayList自动移动需要移动的元素。然后,往内部指定索引的数组位置添加一个元素,然后把size1。继续添加元素,但是数组已满,没有空闲位置的时候,ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组。新数组就有了空位,可以继续添加一个元素到数组末尾,同时size1

      ArrayList把添加和删除的操作封装起来,让我们操作List类似于操作数组,却不用关心内部元素如何移动。

    4、LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素:

            ┌───┬───┐   ┌───┬───┐   ┌───┬───┐   ┌───┬───┐
    HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │   │
            └───┴───┘   └───┴───┘   └───┴───┘   └───┴───┘
    

    5、ArrayListLinkedList:优先使用ArrayList

     ArrayListLinkedList
    获取指定元素 速度很快 需要从头开始查找元素
    添加元素到末尾 速度很快 速度很快
    在指定位置添加/删除 需要移动元素 不需要移动元素
    内存占用 较大

    6、要记住,通过Iterator遍历List永远是最高效的方式。并且,由于Iterator遍历是如此常用,所以,Java的for each循环本身就可以帮我们使用Iterator遍历。

    7、List和Array转换

    (1)调用toArray()方法直接返回一个Object[]数组  —— 丢失类型信息

    (2)给toArray(T[])传入一个类型相同的ArrayList内部自动把元素复制到传入的Array

      如果传入的数组不够大,那么List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null。实际上,最常用的是传入一个“恰好”大小的数组:

    Integer[] array = list.toArray(new Integer[list.size()]);

    (3)最后一种更简洁的写法是通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法

    Integer[] array = list.toArray(Integer[]::new);

    二、编写equals方法

    1、List里新增的元素一定是不同的实例。因此,要正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。

      我们之所以能正常放入StringInteger这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。

    2、在List中查找元素时,List的实现类通过元素的equals()方法比较两个元素是否相等,因此,放入的元素必须正确覆写equals()方法,Java标准库提供的StringInteger等已经覆写了equals()方法;

      编写equals()方法可借助Objects.equals()判断。

      如果不在List中查找元素,就不必覆写equals()方法。

    3、equals()方法的正确编写方法:

      先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;

      用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false

      对引用类型用Objects.equals()比较,对基本类型直接用==比较。

      使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。

      如果不调用Listcontains()indexOf()这些方法,那么放入的元素就不需要实现equals()方法。

    三、Map

    1、Map这种键值(key-value)映射表的数据结构,作用就是能高效通过key快速查找value(元素)。

    2、Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把keyvalue做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap

    3、如果只是想查询某个key是否存在,可以调用boolean containsKey(K key)方法

    4、如果我们在存储Map映射关系的时候,对同一个key调用两次put()方法,分别放入不同的value,会有什么问题呢?重复放入key-value并不会有任何问题,但是一个key只能关联一个value

      牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

    5、MapList不同的是,Map存储的是key-value的映射关系,并且,它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()时放入的key的顺序,也不一定是key的排序顺序。使用Map时,任何依赖顺序的逻辑都是不可靠的。

    四、编写equals和hashCode

    1、在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

    2、要正确使用HashMap,作为key的类必须正确覆写equals()hashCode()方法;

      一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是:

    • 如果equals()返回true,则hashCode()返回值必须相等;

    • 如果equals()返回false,则hashCode()返回值尽量不要相等。

      实现hashCode()方法可以通过Objects.hashCode()辅助方法实现。

    五、EnumMap - 既保证速度,也不浪费空间。

    1、HashMap是一种通过对key计算hashCode(),通过空间换时间的方式,直接定位到value所在的内部数组的索引,因此,查找效率非常高。

    2、如果作为key的对象是enum类型,那么,还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

    Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
    map.put(DayOfWeek.MONDAY, "星期一");

    六、使用TreeMap

    1、有一种Map,它在内部会对Key进行排序,这种Map就是SortedMap。注意到SortedMap是接口,它的实现类是TreeMap

           ┌───┐
           │Map│
           └───┘
             ▲
        ┌────┴─────┐
        │          │
    ┌───────┐ ┌─────────┐
    │HashMap│ │SortedMap│
    └───────┘ └─────────┘
                   ▲
                   │
              ┌─────────┐
              │ TreeMap │
              └─────────┘

    2、SortedMap保证遍历时以Key的顺序来进行排序

    3、使用TreeMap时,放入的Key必须实现Comparable接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。

    七、使用Properties

    1、从文件系统读取.properties文件:

    String f = "setting.properties";
    Properties props = new Properties();
    props.load(new java.io.FileInputStream(f));
    
    String filepath = props.getProperty("last_open_file");
    String interval = props.getProperty("auto_save_interval", "120");

      用Properties读取配置文件,一共有三步:

    1. 创建Properties实例;
    2. 调用load()读取文件;
    3. 调用getProperty()获取配置。

      调用getProperty()获取配置时,如果key不存在,将返回null。我们还可以提供一个默认值,这样,当key不存在的时候,就返回默认值。

    2、如果通过setProperty()修改了Properties实例,可以把配置写入文件,以便下次启动时获得最新配置。写入配置文件使用store()方法:

    Properties props = new Properties();
    props.setProperty("url", "http://www.liaoxuefeng.com");
    props.setProperty("language", "Java");
    props.store(new FileOutputStream("C:\conf\setting.properties"), "这是写入的properties注释");

    3、编码问题:由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取:

    props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));

      就可以正常读取中文。InputStreamReader的区别是一个是字节流,一个是字符流。字符流在内存中已经以char类型表示了,不涉及编码问题。

    4、小结:

      Java集合库提供的Properties用于读写配置文件.properties.properties文件可以使用UTF-8编码。

      可以从文件系统、classpath或其他任何地方读取.properties文件。

      读写Properties时,注意仅使用getProperty()setProperty()方法,不要调用继承而来的get()put()等方法。

    八、Set

    1、如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set

      Set用于存储不重复的元素集合,它主要提供以下几个方法:

    • 将元素添加进Set<E>boolean add(E e)
    • 将元素从Set<E>删除:boolean remove(Object e)
    • 判断是否包含元素:boolean contains(Object e)

      Set实际上相当于只存储key、不存储value的Map

      所以放入Set的元素和Map的key类似,都要正确实现equals()hashCode()方法,否则该元素无法正确地放入Set

      我们经常用Set用于去除重复元素。

    2、Set接口并不保证有序,而SortedSet接口则保证元素是有序的:

    • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
    • TreeSet是有序的,因为它实现了SortedSet接口。
           ┌───┐
           │Set│
           └───┘
             ▲
        ┌────┴─────┐
        │          │
    ┌───────┐ ┌─────────┐
    │HashSet│ │SortedSet│
    └───────┘ └─────────┘
                   ▲
                   │
              ┌─────────┐
              │ TreeSet │
              └─────────┘

      使用TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。

    3、小结:

      Set用于存储不重复的元素集合:放入HashSet的元素与作为HashMap的key要求相同;放入TreeSet的元素与作为TreeMap的Key要求相同;

      利用Set可以去除重复元素;

      遍历SortedSet按照元素的排序顺序遍历,也可以自定义排序算法

    九、使用Queue队列

    1、队列(Queue)是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

    (1)把元素添加到队列末尾;

    (2)从队列头部取出元素。

    2、在Java的标准库中,队列接口Queue定义了以下几个方法:

    • int size():获取队列长度;
    • boolean add(E)/boolean offer(E):添加元素到队尾;
    • E remove()/E poll():获取队首元素并从队列中删除;
    • E element()/E peek():获取队首元素但并不从队列中删除。

      添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。我们用一个表格总结如下:

     throw Exception返回false或null
    添加元素到队尾 add(E e) boolean offer(E e)
    取队首元素并删除 E remove() E poll()
    取队首元素但不删除 E element() E peek()

      两套方法可以根据需要来选择使用。

    3、注意:不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。

    十、使用PriorityQueue - 优先队列

    1、Queue是一个先进先出(FIFO)的队列,但是要实现“VIP插队”的业务,用Queue就不行了,因为Queue会严格按FIFO的原则取出队首元素。我们需要的是优先队列:PriorityQueue

    2、PriorityQueueQueue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

    3、要使用PriorityQueue,我们就必须给每个元素定义“优先级”。

    4、放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。 如果我们要放入的元素并没有实现Comparable接口怎么办?PriorityQueue允许我们提供一个Comparator对象来判断两个元素的顺序。

      PriorityQueue默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)。

    十一、使用Deque - 双端队列

    1、Queue是队列,只能一头进,另一头出。如果把条件放松一下,允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名Deque

    2、Java集合提供了接口Deque来实现一个双端队列,它的功能是:(1)既可以添加到队尾,也可以添加到队首;(2)既可以从队首获取,又可以从队尾获取。

     QueueDeque
    添加元素到队尾 add(E e) / offer(E e) addLast(E e) / offerLast(E e)
    取队首元素并删除 E remove() / E poll() E removeFirst() / E pollFirst()
    取队首元素但不删除 E element() / E peek() E getFirst() / E peekFirst()
    添加元素到队首 addFirst(E e) / offerFirst(E e)
    取队尾元素并删除 E removeLast() / E pollLast()
    取队尾元素但不删除 E getLast() / E peekLast()

    十二、使用Stack

     1、栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。

    2、所谓FIFO,是最先进队列的元素一定最早出队列,而LIFO是最后进Stack的元素一定最早出Stack。如何做到这一点呢?只需要把队列的一端封死。

      因此,Stack是这样一种数据结构:只能不断地往Stack中压入(push)元素,最后进去的必须最早弹出(pop)来

    3、Stack只有入栈和出栈的操作:

    • 把元素压栈:push(E)
    • 把栈顶的元素“弹出”:pop()
    • 取栈顶元素但不弹出:peek()

    4、在Java中,我们Deque可以实现Stack的功能

    • 把元素压栈:push(E)/addFirst(E)
    • 把栈顶的元素“弹出”:pop()/removeFirst()
    • 取栈顶元素但不弹出:peek()/peekFirst()

      为什么Java的集合类没有单独的Stack接口呢?因为有个遗留类名字就叫Stack,出于兼容性考虑,所以没办法创建Stack接口,只能用Deque接口来“模拟”一个Stack了。当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。

    十三、使用Iterator

    1、Java的集合类都可以使用for each循环,ListSetQueue会迭代每个元素,Map会迭代每个key。Java编译器并不知道如何遍历List,只是因为编译器把for each循环通过Iterator改写为了普通的for循环:

    for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
         String s = it.next();
         System.out.println(s);
    }

    2、Iterator是一种抽象的数据访问模型。使用Iterator模式进行迭代的好处有:

    • 对任何集合都采用同一种访问模型;
    • 调用者对集合内部结构一无所知;
    • 集合类返回的Iterator对象知道如何迭代。

      Java提供了标准的迭代器模型,即集合类实现java.util.Iterable接口,返回java.util.Iterator实例。

  • 相关阅读:
    Jenkins 基础篇
    Jenkins 基础篇
    Windows配置Nodejs环境
    Windows配置安装JDK
    Windows安装MySQL
    Ubuntu安装MySQL
    利用中国移动合彩云实现360云盘迁移到百度云
    Linux Shell下的后台运行及其前台的转换
    nova image-list 和 glance image-list 有什么区别
    启动虚拟机时提示我已移动或我已复制选项的详解
  • 原文地址:https://www.cnblogs.com/goloving/p/14813946.html
Copyright © 2011-2022 走看看