集合类
0、简介
-
java.util
包中主要提供了三中集合类型:List
: 有序列表的集合Set
: 没有重复元素的集合Map
: 通过键值(key-value)查找的映射表集合
-
关于
java.util.Collection
:Collection
是除Map
外的所有其它集合类的根接口 -
java集合的特点:
- 一, 接口和实现类分离
- 二, 支持泛型
List[] li = new ArrayList[]; // 可以放任意类型数据 List<String>[] = new ArrayList<>[]; // 只能存放String类型的数据
-
java集合支持通过统一的方式进行访问: 迭代器(Iterator)
-
部分集合属于历史遗留, 不应该继续使用:
Hashtable
: 一种线程安全的Map实现Vector
: 一种线程安全的List实现Stack
: 基于Vector实现的LIFO的栈Enumeration<E>
: 集合接口, 已被Iterator<E>
取代
1、List
-
List
是一种有序链表 -
List<E>
接口:- 末尾添加元素:
void add(E e)
- 指定位置插入元素:
void add(int index, E e)
- 删除指定位置的元素:
int remove(int index)
- 删除指定元素:
int remove(Object e)
- 获取指定位置的元素:
E get(int index)
- 获取列表大小:
int size()
- 末尾添加元素:
-
ArrayList
通过数组实现List
,LinkedList
通过链表实现List
-
List
可以添加重复元素, 可以添加null
-
创建
List
List<String> li = new ArrayList<>(); List<Float> li = new LinkedList<>(); List<Integer> li = List.of(1, 2, 3);
-
列表的遍历, 不推荐使用循环序号遍历, 因为对LinkedList链表效率很差
-
使用迭代器
Iterator
: 内部实现总是采用最高效的方式迭代, 调用方不用关心内部结构import java.util.Iterator; import java.util.List; 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); } for (String s: list) { // 使用for each方法自动调用iterator System.out.println(s); } } }
-
List
和Array
转换:li.toArray()
方法返回Object[]
数组, 数据类型会丢失li.toArray(T[])
方法返回T[]
数组,List
会把元素复制到T[]
并返回:Integer[] arr = li.toArray(new Integer[3]);
- 如果传入
T[]
类型不匹配, 会抛出ArrayStoreException
T[]
大小不够则List内部创建新的T[],T[]
有多余则多余的位置全填充null
Integer[] arr = li.toArray(new Integer[li.size()]);
List<> li = List.of(arr)
, 注意: 该方法返回的是一个只读List
, 修改时会抛出UnsupportedOperationException
equals
List
提供了接口:boolean contains(Object e)
判断列表是否包含元素e
int indexOf(Object o)
查找列表中元素o
的位置, 不存在返回-1
- 以上方法在列表中判断的时候, 都是使用
equals
方法比较, 所以即使的相同值的不同实例对象也能判断出来, 而==
只判断是否为同一对象 - 在自定义的类型列表中使用以上方法, 就必须要正确的覆写自定义类的
equals
方法 - 编写
equals
方法需要满足:- 自反性(Reflexive):对于非null的x来说, x.equals(x)必须返回true
- 对称性(Symmetric):对于非null的x和y来说,如果x.equals(y)为true,则y.equals(x)也必须为true
- 传递性(Transitive):对于非null的x、y和z来说,如果x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)也必须为true
- 一致性(Consistent):对于非null的x和y来说,只要x和y状态不变,则x.equals(y)总是一致地返回true或者false
- 对null的比较:即x.equals(null)永远返回false
- 编写
equals
方法:- 先确定实例“相等”的逻辑
- 用
instanceof
判断传入的待比较的Object是不是属于当前类型 - 对引用类型用
Objects.equals()
比较(可以进行null比较),对基本类型直接用==比较 - 注意要
import java.util.Objects;
- 不需要用到对象的相等判断时, 可以不用覆写
equals
2、Map
-
Map<K, V>
是一种键-值映射表put(K key, V value)
V get(K key)
, 不存在则返回null
-
Map
中的key不重复, value可以重复出现 -
遍历
Map
:keySet()
entrySet()
// 最常用的Map是HashMap(哈希映射表) Map<String, Integer> map = new HashMap<>() for(String s: map.keySet()) { Integer i = map.get(s); } for(Map.Entry<String, Integer> e: map.entrySet()) { String k = e.getKey(); Integer v = e.getValue(); }
-
自定义键值存储, 正确使用
Map
必须保证:-
作为key的对象必须正确覆写
equals()
方法, 相等的两个key实例调用equals()
必须返回true
-
作为key的对象还必须正确覆写
hashCode()
方法, 且hashCode()
方法要严格遵循以下规范:- 如果两个对象相等, 则两个对象的
hashCode()
必须相等 - 如果两个对象不相等, 则两个对象的
hashCode()
尽量不要相等
- 如果两个对象相等, 则两个对象的
-
equals
方法跟List
中的一样,hashCode
方法则可以利用所比较对象的hashCode
方法来实现 -
对于
null
, 与equals
类似可以使用Objects.hash
方法:public int hashCode() { return Objects.hash(name, score, age); } // Objects.hashCode(Object o); // Objects.hash(Object... o);
-
-
HashMap
是通过hash算法实现位置存储, 具有顺序不可预测性
3、EnumMap
-
如果
Map
存储的key是枚举enum
类型的, 可以使用java.util.EnumMap
类型, 可以节省额外的空间浪费(类似一种对应翻译)Map<MyEnum, String> map = new EnumMap<>(MyEnum.class);
4、TreeMap
-
Map
下有一个SortedMap
接口, 内部会对存储的key进行排序, 它的实现类是TreeMap
-
使用
TreeMap
时, 传入的key的类需要实现Comparable
接口 -
java内部类已经实现了
Comparable
, 可以直接使用 -
如果没有实现
Comparable
接口, 可以在创建TreeMap
时传入自定义排序算法:import java.util.*; Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() { public int compare(Person p1, Person p2) { return p1.name.compareTo(p2.name); } });
-
TreeMap
比较规则, key相等返回0, 前面的key与后面的key比较返回-1, 否则返回1 -
在获取数据时,
TreeMap
返回经上述比较后为0的key的value, 所以需要规范的定义比较规则 -
注意:
TreeMap
不依赖equals
和hashCode
进行存储和索引
Properties
-
java支持配置文件
.properties
, 使用Properties
读取 -
配置文件每行以
key=value
的形式存储,#
开头表示注释行 -
Properties
本质上是一种Hashtable
, 但是不要使用继承的put和get方法 -
load(InputStream)/load(Reader)
-
setProperties(String k, String v)
,getProperties(String k)/getProperties(String k, String default)
-
store(OutputStream)
Properties pr = new Properties(); // 直接传入指定的文件字节流 pr.load(new java.io.FileInputStream("setting.properties")); // 传入相对类目录classpath的文件流 pr.load(getClass().getResourceAsStream("/common/setting.properties")); // 传入内存字节流 String setting = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01"; pr.load(new ByteArrayInputStream(setting.getBytes("utf-8"))); // 中文编码, 传入字符流, 不涉及编码问题 pr.load(new FileReader("settings.properties", StandardCharsets.UTF_8)); props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
-
多次load时, 后面的内容会对前面的内容进行覆盖
-
获取配置不存在会返回null, 也可以在获取时设置默认返回值
4、Set
-
Set<E>
用于存储不重复元素的集合:boolean add(E e);
boolean remove(Object e);
boolean contains(Object e);
int size();
-
Set
相当于只存储key不存储value的Map
, 所以也要正确实现equals和hashCode方法 -
Set
接口最常用的实现类是HashSet
,HashSet
实际上是对HashMap
的简单封装 -
与
Map
类似,Set
无序存储,SortedSet
为有序存储接口, 实现类为TreeSet
, 按元素排序顺序存储 -
与
TreeMap
类似,TreeSet
传入的类型需要实现Comparable
接口, 或者在构建是传入自定义比较器Comparator
:Set<MyClass> set = new TreeSet<>(new Comparator<MyClass>() { public int compare(MyClass mc1, MyClass mc2) { return Integer.compare(mc1.num, mc2.num); // return mc1.name.compareTo(mc2.name); } })
5-1、Queue(接口)
-
队列Queue接口实际上是一种先进先出(FIFO)的有序表:
int size()
: 获取队列长度boolean add(E e)
/boolean offer(E e)
: 在队尾添加元素E remove()
/E poll()
: 获取队首元素并从队列中删除E element()
/E peek()
: 获取队首元素但不从队列中删除- 上面3种操作总是有两个不同的方法, 在操作失败发生异常时, 前面的方法抛出异常, 后面的方法返回false或者null
- 注意: 不要将null添加到队列, 否则poll和peek无法判断结果
-
LinkedList
既实现了List
接口, 也实现了Queue
接口, 在使用时,List
引用可以调用List
方法,Queue
引用可以调用Queue
方法// 这是一个List: List<String> list = new LinkedList<>(); // 这是一个Queue: Queue<String> queue = new LinkedList<>();
5-2、PriorityQueue
PriorityQueue
是Queue
接口的特殊实现类,相当于是有序队列, 会根据优先级决定出队顺序- 放入
PriorityQueue
队列的元素类需要实现Comparable
接口, 没有实现则可以在构建时传入Comparator
接口的实现实例
5-3、Deque(接口)
Deque
是一种双端队列(Double Ended Queue), 在队首和队尾都可以入队或出队Deque
接口继承自Queue
接口, 是Queue
的一种扩展addLast(E e)
/offerLast(E e)
E removeFirst()
/E pollFirst()
E getFirst()
/E peekFirst()
addFirst(E e)
/offerFirst(E e)
E removeLast()
/E pollLast()
E getLast()
/E peekLast()
- 在
Deque
中也可以调用Queue
的add和offer方法, 但最好不要使用, 应该使用addLast/offerLast Deque
接口的实现类有ArrayDeque
,LinkedList
- 同样, 对应
LinkedList
应该总是使用特定的接口来引用, 尽量满足面向抽象编程的规范 - 避免将
null
加入Deque
5-4、Stack(接口)
- 栈Stack是一种后进先出(LIFO)的数据结构
- 由于历史遗留问题, java没有创建独立的Stack结构, 可以使用
Deque
实现Stack功能:- 压栈:push(E)/addFirst(E)
- 出栈:pop()/removeFirst()
- 取栈顶元素但不弹出:peek()/peekFirst()
6、Iterator
-
自定义的类想用于for each循环, 就需要实现
Iterable
接口, 返回Iterator
接口实现, 编译器自动改写:for(Iterator<String> it = list.iterator(); it.hasNext(); ) { String s = it.next(); System.out.println(s); }
-
Iterator是一种抽象的数据访问模型, 使用Iterator模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型
- 调用者可以对集合内部结构一无所知
- 集合类返回的Iterator对象知道如何迭代
-
通常外部类实现
Iterable
接口, 用一个内部类来实现Iterator接口,这个内部类可以直接访问对应的外部类的所有字段和方法:- 通过
MyOutterClass.this
内部类可以获取外部类的当前实例对象(有点像获取当前线程) - 内部类需要实现hasNext和next方法
- 而外部类通常只需要返回内部类的实例
- 通过
7、Collections
-
Collections
定义了一系列的静态方法, 能更方便的操作各种集合public static boolean addAll(Collection<? super T> c, T... elements) { ... }
-
创建空集合:
- 创建空
List
:List<T> emptyList()
- 创建空
Map
:Map<K, V> emptyMap()
- 创建空
Set
:Set<T> emptySet()
- 返回的空集合是不可变集合, 与
of(T...)
方法等价
- 创建空
-
创建单元素集合:
- 创建一个元素的
List
:List<T> singletonList(T o)
- 创建一个元素的
Map
:Map<K, V> singletonMap(K key, V value)
- 创建一个元素的
Set
:Set<T> singletonSet(T o)
- 返回的单元素集合也是不可变集合, 与
of(T...)
方法等价
- 创建一个元素的
-
Collections
可以对List
排序:Collections.sort(List l)
- 排序会直接修改list, 所以必须传入可变List
-
洗牌算法:
Collections.shuffle(List l)
-
不可变集合:
Collections
还提供了一组方法把可变集合封装成不可变集合- 封装成不可变
List
:List<T> unmodifiableList(List<? extends T> list)
- 封装成不可变
Set
:Set<T> unmodifiableSet(Set<? extends T> set)
- 封装成不可变
Map
:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
- 这种封装实际上是通过创建一个代理对象, 拦截掉所有修改方法实现的
- 这种方式可以继续修改原集合, 而且会影响封装后的"不可变性"
- 可以通过将原集合的引用赋值为
null
的方式丢弃原引用, 从而保护其不可变性
-
线程安全集合:
- 变为线程安全的
List
:List<T> synchronizedList(List<T> list)
- 变为线程安全的
Set
:Set<T> synchronizedSet(Set<T> s)
- 变为线程安全的
Map
:Map<K,V> synchronizedMap(Map<K,V> m)
- Java 5之后引入了更高效的并发集合类(
Concurrent***
, 上述方法基本已不用
- 变为线程安全的