Java集合知识点总结——List,Map,Set,Queue

<----------------------------廖雪峰学Java-------------------------------->

1. 集合基本概念

  • 如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合
  • Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口
  • Java的java.util包主要提供了以下三种类型的集合:

List:一种有序列表的集合,例如,按索引排列的Student的List;
Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set;
Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map

  • Java集合的设计有几个特点:一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayList,LinkedList等,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素
  • Java访问集合总是通过统一的方式——**迭代器(Iterator)**来实现

2. List

  • List是最基础的一种集合:它是一种有序列表,List内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置
  • 实际应用中,需要增删元素的有序列表,我们使用最多的是ArrayList
  • LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素
  • List<E>接口提供了以下几个主要的接口方法:

在末尾添加一个元素:boolean add(E e)
在指定索引添加一个元素:boolean add(int index, E e)
删除指定索引的元素:E remove(int index)
删除某个元素:boolean remove(Object e)
获取指定索引的元素:E get(int index)
获取链表大小(包含元素的个数):int size()

  • 在这里插入图片描述
  • 可以通过List接口提供的of()方法,根据给定元素快速创建List:
List<Integer> list = List.of(1, 2, 5);
// List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常
  • 可以用for循环根据索引配合get(int)方法遍历List;get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢
  • Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率;boolean hasNext()判断是否有下一个元素,E next()返回下一个元素:
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
            String s = it.next();
            System.out.println(s);
        }
    }
}
  • Java的for each循环本身就可以帮我们使用Iterator遍历:
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        for (String s : list) {
            System.out.println(s);
        }
    }
}
  • List转换为Array的三种方法:
  1. 调用toArray()方法直接返回一个Object[]数组;这种方法会丢失类型信息,所以实际应用很少
  2. 第二种方式是给toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中
public class Main {
   public static void main(String[] args) {
       List<Integer> list = List.of(12, 34, 56);
       Integer[] array = list.toArray(new Integer[3]);
       for (Integer n : array) {
           System.out.println(n);
       }
   }
}
// 如果传入的数组不够大,那么List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null
  1. 通过List接口定义的T[] toArray(IntFunction<T[]> generator)
Integer[] array = list.toArray(Integer[]::new); // 函数式编程
  • 将Array转换为List,直接用List.of(T…)即可;要注意的是,返回的List不一定就是ArrayList或者LinkedList,因为List只是一个接口,如果我们调用List.of(),它返回的是一个只读List
  • List提供了boolean contains(Object o) 方法来判断List是否包含某个指定元素。此外,int indexOf(Object o) 方法可以返回某个元素的索引,如果元素不存在,就返回-1
  • List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等
  • equals()方法要求我们必须满足以下条件:
  1. 自反性(Reflexive):对于非null的x来说,x.equals(x)必须返回true;
  2. 对称性(Symmetric):对于非null的x和y来说,如果x.equals(y)为true,则y.equals(x)也必须为true;
  3. 传递性(Transitive):对于非null的x、y和z来说,如果x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)也必须为true;
  4. 一致性(Consistent):对于非null的x和y来说,只要x和y状态不变,则x.equals(y)总是一致地返回true或者false;
  5. 对null的比较:即x.equals(null)永远返回false。
  • 如果List中保存的是我们自己定义的类对象,那么为了保证List API功能的正常运行,我们要在类中重写equals方法

3. Map

  • Map这种键值(key-value)映射表的数据结构,作用就是能高效通过key快速查找value(元素)
  • Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null
  • Map也是一个接口,最常用的实现类是HashMap
  • 调用boolean **containsKey(K key)**方法查询某个键是否存在
  • Map中不存在重复的key,放入相同的key,只会把原有的key-value对应的value给替换掉
  • 遍历key可以使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合
  • 同时遍历key和value可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射
public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 123);
        map.put("pear", 456);
        map.put("banana", 789);
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println(key + " = " + value);
        }
    }
}
  • Map存储的是key-value的映射关系,它不保证顺序,任何顺序逻辑使用Map实现都是错误的
  • 在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法;如果不赋写equals方法可能出现的情况是:不同的对象(S1与S2),即使对象内容完全相同,在通过Map.get(S1/S2)方法获取value时,会出现不一致甚至无法找到的情况
  • 相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对,通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数
  • 正确使用Map必须保证:
  1. 作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;
  2. 作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:
    如果两个对象相等,则两个对象的hashCode()必须相等
    如果两个对象不相等,则两个对象的hashCode()尽量不要相等
int hashCode() {  // 借助Objects的hash方法实现,避免因为对象字段为空时抛出`NullPointerException`错误
    return Objects.hash(firstName, lastName, age);
}
  • 编写equals()和hashCode()遵循的原则是:equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算
  • 注意点:
  1. 不指定Map的大小,HashMap初始化时默认的数组大小只有16
  2. HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,扩容会导致重新分布已有的key-value
  3. 频繁地扩容会导致Map的效率下降严重,因此最好指定初始大小;HashMap内部的数组长度总是2的n次幂,如指定Map的大小为1000,实际创建的空间大小为1024
  4. 在HashMap的数组中,实际存储的不是一个Person实例,而是一个List,处理键冲突的情况

4. EnumMap

  • 如果作为key的对象是enum类型,那么,还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费
public class Main {
    public static void main(String[] args) {
        Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
        map.put(DayOfWeek.MONDAY, "星期一");
        map.put(DayOfWeek.TUESDAY, "星期二");
        map.put(DayOfWeek.WEDNESDAY, "星期三");
        map.put(DayOfWeek.THURSDAY, "星期四");
        map.put(DayOfWeek.FRIDAY, "星期五");
        map.put(DayOfWeek.SATURDAY, "星期六");
        map.put(DayOfWeek.SUNDAY, "星期日");
        System.out.println(map);
        System.out.println(map.get(DayOfWeek.MONDAY));
    }
}

5.TreeMap

  • 有一种Map,它在内部会对Key进行排序(hashMap是无序的),这种Map就是SortedMap。注意到SortedMap是接口,它的实现类是TreeMap
  • 在这里插入图片描述
  • 使用TreeMap时,放入的Key必须实现Comparable接口,如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法:
public class Main {
    public static void main(String[] args) {
        Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return p1.name.compareTo(p2.name);
            }
        });
        map.put(new Person("Tom"), 1);
        map.put(new Person("Bob"), 2);
        map.put(new Person("Lily"), 3);
        for (Person key : map.keySet()) {
            System.out.println(key);
        }
        // {Person: Bob}, {Person: Lily}, {Person: Tom}
        System.out.println(map.get(new Person("Bob"))); // 2
    }
}

class Person {
    public String name;
    Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "{Person: " + name + "}";
    }
}
  • Comparator接口要求实现一个比较方法,它负责比较传入的两个元素a和b,如果a<b,则返回负数,通常是-1,如果a==b,则返回0,如果a>b,则返回正数,通常是1
  • TreeMap不使用equals()和hashCode()来进行key的匹配
  • TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。在两个Key相等时,必须返回0

6. Properties

  • 配置文件的特点是,它的Key-Value一般都是String-String类型的,因此我们完全可以用Map<String, String>来表示它
  • Java集合库提供了一个Properties来表示一组“配置”
  • Java默认配置文件以.properties为扩展名,每行以key=value表示,以#课开头的是注释
  • 从文件系统读取.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");
  • 调用getProperty()获取配置时,如果key不存在,将返回null。我们还可以提供一个默认值,当key不存在的时候,就返回默认值
  • 可以从classpath读取.properties文件,因为load(InputStream)方法接收一个InputStream实例,表示一个字节流,它不一定是文件流,也可以是从jar包中读取的资源流:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties"));
  • 从内存中读取字节流:
import java.io.*;
import java.util.Properties;

public class Main {
    public static void main(String[] args) throws IOException {
        String settings = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01";
        ByteArrayInputStream input = new ByteArrayInputStream(settings.getBytes("UTF-8"));
        Properties props = new Properties();
        props.load(input);

        System.out.println("course: " + props.getProperty("course"));
        System.out.println("last_open_date: " + props.getProperty("last_open_date"));
        System.out.println("last_open_file: " + props.getProperty("last_open_file"));
        System.out.println("auto_save: " + props.getProperty("auto_save", "60"));
    }
}
  • 有多个.properties文件,可以反复调用load()读取,后读取的key-value会覆盖已读取的key-value:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties"));
props.load(new FileInputStream("C:\\conf\\setting.properties"));
  • Properties的一个常用用法:可以把默认配置文件放到classpath中,然后,根据机器的环境编写另一个配置文件,覆盖某些默认的配置
  • 如果通过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注释");
  • 早期版本的Java规定.properties文件编码是ASCII编码(ISO8859-1),如果涉及到中文就必须用name=\u4e2d\u6587来表示。从JDK9开始,Java的.properties文件可以使用UTF-8编码。需要注意的是,由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取:
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
  • InputStream和Reader的区别是一个是字节流,一个是字符流。字符流在内存中已经以char类型表示了,不涉及编码问题

7. Set

  • Set用于存储不重复的元素集合,它主要提供以下几个方法:
将元素添加进Set<E>boolean add(E e)
将元素从Set<E>删除:boolean remove(Object e)
判断是否包含元素:boolean contains(Object e)
  • 放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set
  • 最常用的Set实现类是HashSetHashSet仅仅是对HashMap的一个简单封装(内部由hashMap实现,key类型自定义,value类型为一个静态的Object对象)
  • Set接口并不保证有序,而SortedSet接口则保证元素是有序的:
HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
TreeSet是有序的,因为它实现了SortedSet接口。
  • 在这里插入图片描述
  • 遍历TreeSet时,输出是有序的
  • TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象

8. Queue

  • Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表
  • Java的标准库中,队列接口Queue定义了以下几个方法:
int size():获取队列长度;
boolean add(E)/boolean offer(E):添加元素到队尾;
E remove()/E poll():获取队首元素并从队列中删除;
E element()/E peek():获取队首元素但并不从队列中删除。
  • 对于具体的实现类,有的Queue有最大队列长度限制,有的Queue没有
  • 注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的:
    在这里插入图片描述
  • LinkedList即实现了List接口,又实现了Queue接口,但是,在使用的时候,如果我们把它当作List,就获取List的引用,如果我们把它当作Queue,就获取Queue的引用:
// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();

9. PriorityQueue

  • PriorityQueue和Queue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素
  • 放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级;PriorityQueue允许我们提供一个Comparator对象来判断两个元素的顺序
public class Main {
    public static void main(String[] args) {
        Queue<User> q = new PriorityQueue<>(new UserComparator());
        // 添加3个元素到队列:
        q.offer(new User("Bob", "A1"));
        q.offer(new User("Alice", "A2"));
        q.offer(new User("Boss", "V1"));
        System.out.println(q.poll()); // Boss/V1
        System.out.println(q.poll()); // Bob/A1
        System.out.println(q.poll()); // Alice/A2
        System.out.println(q.poll()); // null,因为队列为空
    }
}

class UserComparator implements Comparator<User> {
    public int compare(User u1, User u2) {
        if (u1.number.charAt(0) == u2.number.charAt(0)) {
            // 如果两人的号都是A开头或者都是V开头,比较号的大小:
            return u1.number.compareTo(u2.number);
        }
        if (u1.number.charAt(0) == 'V') {
            // u1的号码是V开头,优先级高:
            return -1;
        } else {
            return 1;
        }
    }
}

class User {
    public final String name;
    public final String number;

    public User(String name, String number) {
        this.name = name;
        this.number = number;
    }

    public String toString() {
        return name + "/" + number;
    }
}

10. Deque

  • 允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名Deque
  • Queue与Deque方法对比:
    对比
  • Deque接口实际上扩展自Queue:
public interface Deque<E> extends Queue<E> {
    ...
}

因此Deque可以使用Queue的方法,但是不建议这么使用

  • Deque是一个接口,它的实现类有ArrayDequeLinkedList

11. Stack

  • 栈(Stack)是一种后进先出(LIFO:Last In First Out) 的数据结构
  • Stack只有入栈和出栈的操作:
把元素压栈:push(E);
把栈顶的元素“弹出”:pop();
取栈顶元素但不弹出:peek()
  • Java的集合类没有单独的Stack接口,用Deque可以实现Stack的功能:
把元素压栈:push(E)/addFirst(E);
把栈顶的元素“弹出”:pop()/removeFirst();
取栈顶元素但不弹出:peek()/peekFirst()

注意:只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰

  • JVM在处理Java方法调用的时候就会通过栈这种数据结构维护方法调用的层次,每调用一个方法时,先将参数压栈,然后执行对应的方法;当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值。因为方法调用栈有容量限制,嵌套调用过多会造成栈溢出,即引发StackOverflowError
  • 栈用于进制转换
  • 栈用于计算后缀表达式(遇到运算符时弹出栈中元素进行运算,运算结束后将结果压栈,如此反复)

12. Iterator

  • Java的集合类都可以使用for each循环,List、Set和Queue会迭代每个元素,Map会迭代每个key
  • 编译器把for each循环通过Iterator改写为了普通的for循环:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
     String s = it.next();
     System.out.println(s);
}
  • 这种通过Iterator对象遍历集合的模式称为迭代器
  • Iterator对象是集合对象自己在内部创建的,知道如何高效遍历内部的数据集合,调用方则获得了统一的代码
  • 如果我们自己编写了一个集合类,想要使用for each循环,只需满足以下条件:
1. 集合类实现Iterable接口,该接口要求返回一个Iterator对象;
2.Iterator对象迭代集合内部数据。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ReverseList<String> rlist = new ReverseList<>();
        rlist.add("Apple");
        rlist.add("Orange");
        rlist.add("Pear");
        for (String s : rlist) {
            System.out.println(s);
        }
    }
}

// 一个以反方向遍历的集合
class ReverseList<T> implements Iterable<T> {

    private List<T> list = new ArrayList<>();

    public void add(T t) {
        list.add(t);
    }

    @Override
    public Iterator<T> iterator() {
        return new ReverseIterator(list.size());
    }

    class ReverseIterator implements Iterator<T> {
        int index;

        ReverseIterator(int index) {
            this.index = index;
        }

        @Override
        public boolean hasNext() {
            return index > 0;
        }

        @Override
        public T next() {
            index--;
            // 通过Outer.this访问外部对象及其对应的方法以及字段
            return ReverseList.this.list.get(index);
        }
    }
}

13. Collections

  • Collections是JDK提供的工具类,同样位于java.util包中。它提供了一系列静态方法,能更方便地操作各种集合;注意不是Collection接口
  • addAll() 方法可以给一个Collection类型的集合添加若干元素。因为方法签名是Collection,所以我们可以传入List,Set等各种集合类型:
public static boolean addAll(Collection<? super T> c, T... elements) { ... }
  • Collections提供了一系列方法来创建空集合
创建空ListList<T> emptyList()
创建空MapMap<K, V> emptyMap()
创建空SetSet<T> emptySet()

返回的空集合是不可变集合,无法向其中添加或删除元素
也可以用各个集合接口提供的of(T…)方法创建空集合:List.of()

  • Collections提供了一系列方法来创建一个单元素集合
创建一个元素的ListList<T> singletonList(T o)
创建一个元素的MapMap<K, V> singletonMap(K key, V value)
创建一个元素的SetSet<T> singleton(T o)

返回的单元素集合也是不可变集合,无法向其中添加或删除元素
也可以用各个集合接口提供的of(T…)方法创建单元素集合

  • Collections可以对List进行排序。因为排序会直接修改List元素的位置,因此必须传入可变List:
public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("pear");
        list.add("orange");
        // 排序前:
        System.out.println(list);
        Collections.sort(list);
        // 排序后:
        System.out.println(list);
    }
}
  • Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序:
public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i=0; i<10; i++) {
            list.add(i);
        }
        // 洗牌前:
        System.out.println(list);
        Collections.shuffle(list);
        // 洗牌后:
        System.out.println(list);
    }
}
  • Collections还提供了一组方法把可变集合封装成不可变集合:
封装成不可变ListList<T> unmodifiableList(List<? extends T> list)
封装成不可变SetSet<T> unmodifiableSet(Set<? extends T> set)
封装成不可变MapMap<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)

实际上是通过创建一个代理对象,拦截掉所有修改方法实现的
注意:方法返回的集合是不可变的,但是原集合仍然是可变的

  • Collections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:
变为线程安全的ListList<T> synchronizedList(List<T> list)
变为线程安全的SetSet<T> synchronizedSet(Set<T> s)
变为线程安全的MapMap<K,V> synchronizedMap(Map<K,V> m)

从Java 5开始,引入了更高效的并发集合类,所以上述这几个同步方法已经过时

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值