第七章 Java 集合
7.1.0 概述
Java集合类存放于 java.util 包中,是一个用来存放对象的容器。
Java 集合可分为 Set、List 和 Map 三种大体系
- Set:无序、不可重复的集合
- List:有序,可重复的集合——动态数组
- Map:具有映射关系的集合
①、集合只能存放对象。比如你存一个 int 型数据 1放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。
②、集合存放的是多个对象的引用,对象本身还是放在堆内存中。
③、集合可以存放不同类型,不限数量的数据类型。
在Java中,不同的集合其底层对应不同的数据结构。向不同的集合中存储元素,等于将书籍放到了不同的数据结构当中。
集合存储方式分为两种:Collection单个数据、Map键值对
7.2.0 List
- List 代表一个元素有序、且可重复的集合,集合中的每个元素都有其对应的顺序索引
- List 允许使用重复元素,可以通过索引来访问指定位置的集合元素。
- List 默认按元素的添加顺序设置元素的索引。
- List 集合里添加了一些根据索引来操作集合元素的方法
List<String> list = new ArrayList<String>();
list.add("f");//第一个,索引下标0
list.add("d");
list.add("a");
list.add("c");
list.add("c");//允许重复
System.out.println(list);
System.out.println(list.get(2));//通过索引来访问指定位置的集合元素
list.add(1,"t");//在指定索引下标位置插入数据
System.out.println(list);
List<String> l = new ArrayList<String>();
l.add("123");//第一个,索引下标0
l.add("456");
list.addAll(2, l);//在指定索引下标位置插入集合
System.out.println(list);
System.out.println(list.indexOf("a"));
//获取指定元素在集合中第一次出现的索引下标
System.out.println(list.lastIndexOf("c"));
//获取指定元素在集合中最后一次出现的索引下标
list.remove(2);//移除下标为2的元素
System.out.println(list);
list.set(0, "ff");//根据指定索引下标修改元素
System.out.println(list);
List<String> subList = list.subList(2, 4);
System.out.println(subList);
//根据索引下标的起始位置截取一段元素,从2开始,不包括4
System.out.println(list.size());
ArrayList
ArrayList。可调整大小的数组实现。初始化容量为10。
底层是数组,尽管可以扩容,但是效率较低,尽量提前定义好容量。
是一种特殊的数据类型:泛型
方法名 | 说明 |
---|---|
Public ArrayList() | 创建一个空的集合对象 |
Public Boolean add(E e) | 将指定的元素追加到此集合的末尾 |
Public void add(int index, E element) | 在此集合中的指定位置插入指定的元素 |
ArrayList底层采用了数组这种数据结构,他是非线程安全的
public ArrayList() //构造一个初始容量为 10 的空列表。
public ArrayList(int initialCapacity) //构造一个具有指定初始容量的空列表。
public ArrayList(Collection<? extends E> c) //构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
LinkedList
底层采用了双向链表的数据结构。
Vector
底层采用了数组的数据结构。默认容量为10,容量满之后双倍扩容。
Vector 是线程安全的。但即使为保证 List 集合线程安全,也不推荐使用 Vector
7.3.0 Set
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。我们大多数时候说的set集合指的都是HashSet。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。
HashSet初始化容量16,建议是2的倍数,扩容后是原容量的2倍。
HashSet 具有以下特点:
- 不能保证元素的排列顺序
- hashcode不能重复
- HashSet 不是线程安全的
- 集合元素可以使 null
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值决定该对象在 HashSet 中的存储位置。
如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
s.add(2);//给集合添加元素
s.add("a");
System.out.println(s);
s.remove(2);//移除元素
System.out.println(s);
System.out.println(s.contains("a"));//判断是否包含元素
s.clear();//清空集合
System.out.println(s);
s.add("a");
s.add("b");
s.add("c");
s.add("d");
s.add(null);//set集合可以存null
System.out.println(s);
7.4.0 Map
Map 和 Collection 没有继承关系。
Map 用于保存具有映射关系的数据,因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存 Map 里的 Value。Map 中的 key 和 value 都可以是任何引用类型的数据。key和value都是存储对象的内存地址,key起主导地位。
Map 中的 Key 不允许重复,即同一个 Map 对象的任何两个 Key 通过 equals 方法比较中返回 false
Key 和 Value 之间存在单向一对一关系,即通过指定的 Key 总能找到唯一的,确定的 Value。
HashMap底层是哈希表,非线程安全。
HashTable底层也是哈希表,是线程安全的,其中所有方法都有synchronized关键字,效率较低。
public interface Map<K,V>
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
// map.clear();
System.out.println(map.containsKey(1));//判断是否含有此key
System.out.println(map.containsValue("a"));//判断是否含有此value
System.out.println(map.get(1));//通过key获取value
System.out.println(map);
System.out.println(map.isEmpty());//如果此映射未包含键-值映射关系,则返回 true。
System.out.println(map.keySet());//返回此映射中包含的键的 Set 视图
System.out.println(map.entrySet());//返回此映射中包含的映射关系的 Set 视图。
System.out.println(map.hashCode());//返回此映射的哈希码值。
// System.out.println(map.remove(3));//
System.out.println(map.remove(3, "c"));//移除指定键值对,Boolean类型
System.out.println(map.values());//返回此映射中包含的值的 Collection 视图。
//遍历map集合
Set<Integer> keys = map.keySet();//获取map集合的key集合
map.values();//获取集合的所有value
for(Integer key : keys) {//forEach遍历
System.out.println("key:" + key + ", value:" + map.get(key));
}
Iterator<Integer> it =keys.iterator();//迭代器遍历
while(it.hasNext()) {
Integer key = it.next();
String value = map.get(key);
System.out.println("key:" + key + ", value:" + value);
}
}
//通过map.entrySet(); 效率更高,key和value都是从node中获取的,适合大量数据。
Set<Entry<String, Integer>> entrys = map.entrySet();
for(Entry<String, Integer> en : entrys) {
System.out.println("key:" + en.getKey() + ", value:" + en.getValue());
}
7.4.1 hashMap
基于哈希表/散列表的 Map
接口的实现。哈希表是一个数组和单向链表的结合体。
哈希值:是key的hashCode()方法的执行结果,hash值是通过哈希函数/算法,可以转换成数组的下标。
同一个链表上,o1和o2的int hash 是相同的。hash相同一定在同一个链表。
若o1和o2的int hash 不同,但由于哈希算法执行后转换的数组下标可能相同。产生哈希碰撞。
存疑:哈希碰撞
若所有的hashCode()方法返回值统一,那么哈希表就变成了单向链表。称为散列分布不均匀。若所有的hashCode()方法返回值都不一样,那么哈希表就变成了数组。
map.get(k)实现原理:
先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上。若这个位置上什么也没有,返回null。若有单向链表,那么会拿着参数k和单向链表上的每一个节点中的k进行equals,若所有的equals方法返回false,那么get返回null;只要有一个节点的key和参数k的equals返回true,那么这个节点的value就是要获取的value。get方法最终返回这个value。
为什么哈希表的随机增删以及查询效率高?
增删是在链表上完成,查询只是扫描部分。hashMap集合的kay,会先后调用两个方法,hashCode() 和 equals() ,而equals默认比较地址,此处需要比较内容,那么这两个方法都要重写。
hashMap集合key部分的特点:
- 无序:不一定存到哪个单向链表。
- 不可重复:通过equals来保证不重复。若重复会覆盖value。
- 放在hashMap集合key部分的元素其实是放在了hashSet集合中,所以HashSet集合中的元素也需要重写hashCode() 和 equals() 。
- key的null值只能有一个。
HashMap集合的默认初始化容量是16,默认加载因子是0.75。即当底层数组容量达到75%时,数组开始扩容。HashMap集合的默认初始化容量必须是2的倍数,这是为了达到散列均匀,提高HashMap的存取效率。
在jdk8之后,如果哈希表的单向链表上的元素大于8个,单向链表这个数据结构会变成红黑树。当红黑树上的节点小于6时,会把红黑树变成单向链表的数据结构。
static final int treeify_threshold = 8;
static final int untreeify_threshold = 6;
7.4.2 重写hashCode和equals
重点:放在hashMap集合key部分的元素以及HashSet集合中的元素,需要同时重写hashCode和equals方法。
如果一个类的equals方法重写了,必须重写HashCode方法。并且equals方法返回true,说明在同一个单向链表上比较。对于同一个单向链表来说,他们的哈希值是相同的,所以hashCode方法返回的值也一样。
HashMap & Hashtable
-
HashMap 和 Hashtable 是 Map 接口的两个典型实现类
-
二者的底层都是哈希表,但hashTable的初始化容量是11,默认加载因子是0.75f。扩容后是:原容量*2+1。
-
区别:
-
- Hashtable 是一个古老的 Map 实现类,不建议使用
- Hashtable 是一个线程安全的 Map 实现,但 HashMap 是线程不安全的。
- Hashtable 不允许使用 null 作为 key 和 value,而 HashMap 可以
- Hashtable 是一个古老的 Map 实现类,不建议使用
-
与 HashSet 集合不能保证元素的顺序一样,Hashtable 、HashMap 也不能保证其中 key-value 对的顺序
-
Hashtable 、HashMap 判断两个 Key 相等的标准是:两个 Key 通过 equals 方法返回 true,hashCode 值也相等。
-
Hashtable 相等的标准是:两个 Value 通过 equalHashMap 判断两个 Values 方法返回 true
7.4.3 Properties
是一个Map集合,继承Hashtable。它的key和value都是String类型。
Properties被称为属性类集合。
Properties是线程安全的。
7.4.4 TreeMap
TreeSet和TreeMap的关系
与HashSet完全类似,TreeSet里面绝大部分方法都市直接调用TreeMap方法来实现的。TreeMap是TreeSet的底层结构
相同点:
- TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步
- 运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而HashMap/HashSet则为O(1)。
- TreeMap和TreeSet都是有序的集合,也就是说他们存储的值都是拍好序的。
不同点:
- 最主要的区别就是TreeSet和TreeMap分别实现Set和Map接口
- TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)
- TreeSet中不能有重复对象,而TreeMap中可以存在
- TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。
底层是二叉树,放到TreeSet集合中的元素,无序不可重复,但是可以按照元素的大小自动排序。称为可排序集合。按照字典顺序,升序。
对于自定义的类型来说,TreeSet无法进行排序。定义比较规则后可以排序。
放在TreeSet集合中的元素需要实现java.lang.Comparable接口。并实现compareTo方法,equals可以不写。
compareTo的返回值:返回0,value会覆盖;返回>0 ,继续在右子树找;返回<0 ,继续在左子树找。
重写compareTo实现排序:java.lang.Comparable接口
public class TreeSetTest {
public static void main(String[] args) {
User u1 = new User(12);
User u2 = new User(22);
User u3 = new User(42);
User u4 = new User(32);
TreeSet<User> users = new TreeSet<>();
users.add(u1);
users.add(u2);
users.add(u3);
users.add(u4);
for(User u : users) {
System.out.println(u);
}
}
}
//放在TreeSet集合中的元素需要实现java.lang.Comparable接口。并实现compareTo方法,equals可以不写。
class User implements Comparable<User>{
int age;
public User(int age) {
this.age = age;
}
@Override//编写比较的逻辑/规则
public int compareTo(User u) {//u1.compareTo(u2);
//this是u1 u是u2
// int age1 = this.age;
// int age2 = u.age;
// if(age1 == age2) {
// return 0;
// }else if(age1 > age2) {
// return 1;
// }else {
// return -1;
// }
return this.age - u.age;
}
@Override
public String toString() {
return "User [age=" + age + "]";
}
}
使用比较器排序:**java.util.Comparator接口 **
public class TreeSetTest02 {
public static void main(String[] args) {
/*
* 创建treeSet集合的时候,需要用比较器
* TreeSet<fish> fishes = new TreeSet<>();//需要传入比较器
*/
// TreeSet<fish> fishes = new TreeSet<>(new fishComparator());
//通过匿名内部类实现Comparator(lambda)
TreeSet<fish> fishes = new TreeSet<>(new Comparator<fish>() {
@Override
public int compare(fish o1, fish o2) {
// 指定比较规则。按照年龄排序
return o1.age - o2.age;
}
});
fishes.add(new fish(123));
fishes.add(new fish(345));
fishes.add(new fish(253));
fishes.add(new fish(563));
for(fish f : fishes) {
System.out.println(f);
}
}
}
class fish{
int age;
public fish(int age) {
this.age = age;
}
@Override
public String toString() {
return "fish [age=" + age + "]";
}
}
//比较器实现java.util.Comparator接口
//class fishComparator implements Comparator<fish>{
// @Override
// public int compare(fish o1, fish o2) {
// // 指定比较规则。按照年龄排序
// return o1.age - o2.age;
// }
//}
结论:放到TreeSet或者TreeMap集合key部分的元素想要做到排序,有两种方式:
放在集合中的元素实现java.lang.Comparable接口。
在构造TreeSet或者TreeMap集合的时候给他传一个比较器。
当比较规则不会发生改变,或者说比较规则只有一个时,建议使用Comparable。
若比较贵者有多个,并且需要在多个比较器之前频繁切换,建议使用Comparator。
7.5.0 Collection
Collections 是一个操作 Set、List 和 Map 等集合的工具类
Collections 中提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
在没有使用泛型之前,Collection中可以存储Object的所有子类型。
7.5.1 contains
底层调用了equals方法来判断内存地址是否相同。放在集合中的元素要重写equals方法,这样会比较内容,否则比较内存地址。
boolean contains(Object o)
如果此 collection 包含指定的元素,则返回 true。更确切地讲,当且仅当此 collection 至少包含一个满足 (onull ? enull : o.equals(e)) 的元素 e 时,返回 true。
参数:o - 测试在此 collection 中是否存在的元素。
返回:如果此 collection 包含指定的元素,则返回 true
抛出:
ClassCastException - 如果指定元素的类型与此 collection 不兼容(可选)。
NullPointerException - 如果指定的元素为 null,并且此 collection 不允许 null 元素(可选)。
7.5.2 集合工具类 Collections
sort(List list) 根据元素的自然顺序 对指定列表按升序进行排序。(要保证list集合中的元素实现Comparable接口)
7.6.0 接口 Iterator
public interface Iterator
对 collection 进行迭代的迭代器。当集合结构发生改变,集合的迭代器必须重新获取。在迭代元素的过程中,要使用迭代器的remove方法。
hasNext
boolean hasNext()
如果仍有元素可以迭代,则返回 true。(换句话说,如果 next 返回了元素而不是抛出异常,则返回 true)。
返回:如果迭代器具有多个元素,则返回 true。
next
E next()
返回:迭代的下一个元素。
抛出: NoSuchElementException - 没有元素可以迭代。
remove
void remove()
从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)。每次调用 next 只能调用一次此方法。如果进行迭代时用调用此方法之外的其他方式修改了该迭代器所指向的 collection,则迭代器的行为是不确定的。
抛出:
UnsupportedOperationException - 如果迭代器不支持 remove 操作。
IllegalStateException - 如果尚未调用 next 方法,或者在上一次调用 next 方法之后已经调用了 remove 方法。