一.为何要用集合?
用数组存储,一旦初始化以后,其长度就确定了,元素的类型也确定了,我们只能操作指定类型的数据,而且是有限个。
而且数组提供的方法非常有限,不适合添加,插入和删除等操作。对于无序、不可重复地存数据的需求不能满足。
针对数组存储的缺点,集合就应运而生了
二.Collection集合继承结构图
如果两者之间有冲突,以下面的为准
三.Collection集合的常用方法
1.非常常用的,也是很简单的(5个)
//声明一个集合
Collection col = new ArrayList();
//add方法
col.add("123");
col.add(1);
col.add(new Date());
//获取元素个数
System.out.println(col.size());//打印3
//声明另外一个集合
Collection col2 = new ArrayList();
col2.add("111");
col2.add(1);
//addAll方法
col.addAll(col2);
System.out.println(col.size());//打印5
System.out.println(col);//直接打印
//isEmpty()方法
System.out.println(col.isEmpty());//false
//clear()方法,清空元素
col.clear();
//isEmpty()方法
System.out.println(col.isEmpty());//true
2.Contains与ContainsAll
需要注意的是:Contains方法比较的是内容而不是地址。
//声明一个集合
Collection col = new ArrayList();
col.add("123");
col.add(1);
col.add(false);
col.add(new Person("张三",18));
//关于contains方法
//我们在判断时会调用obj对象所在类的equals()
//所以向Collection接口的实现类的对象中(比如ArrayList)添加数据obj时,要求obj所在类要重写equals方法
//调用contains方法
//★注意:contains比较的是内容而不是地址
System.out.println(col.contains("123"));//true
System.out.println(col.contains(new Person("张三",18)));
/*
总共比较了四次,因为加了四个对象,而且第四个对象才与要比较的对象是“一样的”
调用了equals方法
调用了equals方法
调用了equals方法
调用了equals方法
true
*/
Collection col2 = new ArrayList();
col2.add("111");
col2.add(11111);
col.addAll(col2);
System.out.println(col.containsAll(col2));//true
3.remove和removeAll
//声明一个集合
Collection col = new ArrayList();
col.add("123");
col.add(1);
col.add(false);
col.add(new Person("张三",18));
System.out.println(col);
//remove
col.remove("123");//调用了String类的equals方法,但是我这里没有重写,也就是没有输出
col.remove(new Person("张三",18));//这里会调用Person类的equals方法,删除“123”后还需要比较三次,否则需要比较四次
System.out.println(col);
Collection col1 = new ArrayList();
col1.add("123");
col1.add(1);
//removeAll:从数学角度来说,就是求差集
col.removeAll(col1);
System.out.println(col);
/*
[123, 1, false, com.day02.Person@16e8e0a]
调用了equals方法
调用了equals方法
调用了equals方法
[1, false]
[false]
*/
4.retainAll求交集和equals
//声明一个集合
Collection col = new ArrayList();
col.add("123");
col.add(1);
col.add(false);
col.add(new Person("张三",18));
System.out.println(col);//[123, 1, false, com.day02.Person@16e8e0a]
//创建col1集合
Collection col1 = new ArrayList();
col1.add("123");
col1.add(1);
//equals方法
System.out.println(col.equals(col1));//false
//求集合之间的交集,并且把当前集合修改
col.retainAll(col1);
System.out.println(col);//[123, 1]
//equals
System.out.println(col.equals(col1));//true
4.hashCode,toArray以及拓展的asList
//声明一个集合
Collection col = new ArrayList();
col.add("123");
col.add(1);
col.add(false);
col.add(new Person("张三",18));
System.out.println(col);//[123, 1, false, com.day02.Person@16e8e0a]
//hashCode
//返回当前对象的哈希值
System.out.println(col.hashCode());//1475509157
//toArray,将集合转化为数组
//这个是Object类型的数组
Object[] objs = col.toArray();
for (Object obj: objs){
System.out.println(obj);
}
/*
123
1
false
com.day02.Person@16e8e0a
*/
//拓展:将数组转化为集合
List<String> strings = Arrays.asList(new String[]{"aa", "bb", "cc"});
System.out.println(strings);//[aa, bb, cc]
//注意:asList方法的一个坑,就是如果是int数组,它会当成是一个元素
List<int[]> ints = Arrays.asList(new int[]{111, 222});
System.out.println(ints + "|" + ints.size());//[[I@34c45dca]|1
//如果想让它当成是多个元素:
List<Integer> integers = Arrays.asList(123, 456);
System.out.println(integers + "|" + integers.size());//[123, 456]|2
5.Iterator以及集合的遍历
(1)简介
首先需要知道迭代器模式是什么
GOF给迭代器模式定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需要暴露该对象的内部细节。迭代器模式,就是为容器而生
而Iterator对象就被称为迭代器,主要用于遍历Collection集合中的元素。(当然,也可以用增强for循环来遍历)
(2)遍历示例
//声明一个集合
Collection col = new ArrayList();
col.add("123");
col.add(1);
col.add(false);
col.add(new Person("张三",18));
//获取迭代器对象
Iterator iterator = col.iterator();
//方式一遍历(不这样用)
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
/*
123
1
false
com.day02.Person@16e8e0a
*/
//System.out.println(iterator.next());//NoSuchElementException
//方式二遍历(也不这样用)
for (int i = 0;i < col.size();i++){
System.out.println(iterator.next());
}
//方式三遍历
while (iterator.hasNext()){
System.out.println(iterator.next());
}
(3)原理介绍
原理在之前没学数据结构的时候不太清楚,但是现在很清楚,它就类似于链表的一个遍历。这里就不再介绍了。
需要注意的一点是:有时候需要重新获取Iterator,而不是用原来的指针,因为那个是一直往后赋值的。
四.Collection子接口之一:List接口
1.简介
List接口常被称为动态数组,里面的元素有序、可重复。
实现类有ArrayList、LinkedList和Vector
2.三个具体实现类的异同
相同点:
三个类都实现了List接口,存储数据的特点相同(即有序可重复)
不同点:
类 | 特点 |
---|---|
ArrayList | List接口的主要实现类,是线程不安全的,效率高,底层使用Object数组存储。 |
LinkedList | 底层使用双向链表存储,如果需要频繁插入删除操作时,使用效率较高。 |
Vector | List接口的古老实现类(不用了),是线程安全的,效率低,底层使用Object数组存储。 |
3.ArrayList底层源码
其实学完数据结构后就很好理解,
jdk7:它一开始是创建了容量是10的Object数组(jdk8,当数组容量不够的时候会进行扩容,默认扩为原来的1.5倍。(类似于单例模式的饿汉式)
jdk8:一开始并没有创建长度为10的数组,而是初始化为{},在第一次add的时候才进行创建,后续操作和jdk7无异。(类似于单例模式的懒汉式)
启示:
①在开发中,如果想要其效率高一些(说白了就是减少它的扩容次数),建议使用其带参的构造器(其参数就是容量),所以还有一个要求就是要提前预估其容量。
②jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
4.LinkedList底层源码
和数据结构双向链表的基本操作几乎是一样的。
5.List接口的常用方法
除了Collection中的常用方法之外,由于List是有序可重复的,所以它又定义了一些其他的方法。
ArrayList list = new ArrayList();
list.add(123);
list.add("111");
list.add(true);
list.add(new Person("张三",18));
//add按照索引值添加
list.add(1,"BB");
System.out.println(list);//[123, BB, 111, true, com.day02.Person@16e8e0a]
//get
System.out.println(list.get(1));//BB
//indexOf,返回首次出现的索引位置,如果不存在,返回-1
System.out.println(list.indexOf("111"));//2
//lastIndexOf:最后一次出现的索引值
System.out.println(list.lastIndexOf("111"));//2
//remove,按照索引删除
list.remove(1);
System.out.println(list);//[123, 111, true, com.day02.Person@16e8e0a]
//set:设置
list.set(1,"1234");
System.out.println(list);//[123, 1234, true, com.day02.Person@16e8e0a]
//subList:返回一个子集合,左闭右开
System.out.println(list.subList(1,3));//[1234, true]
6.总结:List最常用的方法
增: add(Object obj)
删: remove(int index) / remove(Object obj)
改: set(int index,Object ele)
查: get(int index)
插: add(int index,Object ele)
长度: size()
遍历:①Iterator迭代器方式 ②增强for循环 ③普通循环(List特有的)
五.Collection子接口之二:Set接口
set接口不像List接口,没有定义自己新的方法
1.三个实现类的对比
实现类 | 特点 |
---|---|
HashSet | Set的主要实现类,是线程不安全的,可以存储null值 |
LinkedHashSet | 是HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历 |
TreeSet | 可以按照添加对象的指定属性进行排序,底层采用红黑树来存储 |
2.Set无序、不可重复的理解
以HashSet为例
无序性:
①不是随机性,每次遍历还是那个顺序
②存储的数据在底层数组中并非按照索引的顺序添加(我猜测是哈希表存储),而是由数据的哈希值决定的
比如下面这个图,上面的是有序的数组存储,下面就是“无序”的哈希表存储。学完数据结构后就很好理解。
不可重复性:
保证添加的元素按照equals方法判断时,不能返回true,即相同的元素只能添加一个(必须重写equals和hashCode方法)
所以:要求:向Set中添加的元素,其所在类一定要重写equals方法和hashCode方法,而且重写的这两个方法尽可能保持一致性,即相等的对象必须具有相等的散列码。(直接用idea的快捷方式生成就可以)
3.TreeSet的使用
TreeSet可以按照添加对象的指定属性进行排序,所以向TreeSet中添加的数据,要求是相同类的对象。也就是说必须是同一类型。
自然排序
实现Comparable接口
它的比较是根据compareTo方法进行比较的,而不是equals。
如果compareTo方法返回的是0,则说明两个是一样的,就不再进行添加了。
①比如都是整型:
Set a = new TreeSet();
a.add(1);
a.add(-1);
a.add(99);
a.add(-999);
Iterator iterator = a.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
输出结果就是
-999
-1
1
99
②如果是添加对象
Set a = new TreeSet();
a.add(new Person("Jack",18));
a.add(new Person("Hurry",17));
a.add(new Person("Aric",99));
a.add(new Person("OldMan",101));
Iterator iterator = a.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
那么Person类必须实现Comparable接口,这里我按照姓名进行排序,则实现的具体方法为
//按照姓名大小进行排序
@Override
public int compareTo(Object o) {
if(o instanceof Person){
Person person = (Person)o;
return this.name.compareTo(person.name);
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
则遍历得到的结果为
Person{name='Aric', num=99}
Person{name='Hurry', num=17}
Person{name='Jack', num=18}
Person{name='OldMan', num=101}
③第②种情况的延申
可以发现,有两个人名字一样,但是年龄不一样,按照刚刚的方法就有很大的局限性了,所以在这里还需要将刚刚的compareTo方法进行改进
Set a = new TreeSet();
a.add(new Person("Jack",18));
a.add(new Person("Hurry",17));
a.add(new Person("Aric",99));
a.add(new Person("OldMan",101));
a.add(new Person("OldMan",99));
Iterator iterator = a.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
改进后的compareTo方法(如果姓名相同,比年龄)
//按照姓名大小进行排序
@Override
public int compareTo(Object o) {
if(o instanceof Person){
Person person = (Person)o;
int compare = this.name.compareTo(person.name);
if(compare != 0){
return compare;
}else {
return Integer.compare(this.num,person.num);
}
}else {
throw new RuntimeException("输入的类型不匹配");
}
}
则输出结果为
Person{name='Aric', num=99}
Person{name='Hurry', num=17}
Person{name='Jack', num=18}
Person{name='OldMan', num=99}
Person{name='OldMan', num=101}
定制排序
它的比较是根据compare方法进行比较的,而不是equals。
Comparator com = new Comparator() {
//按照年龄大小进行排序
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Person && o2 instanceof Person) {
Person p1 = (Person) o1;
Person p2 = (Person) o2;
return Integer.compare(p1.num, p2.num);
}else {
throw new RuntimeException("类型不匹配");
}
}
};
Set a = new TreeSet(com);
a.add(new Person("Jack",18));
a.add(new Person("Hurry",17));
a.add(new Person("Aric",99));
a.add(new Person("OldMan",101));
a.add(new Person("OldMan",99));
Iterator iterator = a.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
输出的结果为
Person{name='Hurry', num=17}
Person{name='Jack', num=18}
Person{name='Aric', num=99}
Person{name='OldMan', num=101}
六.小总结
如果集合种存储的是自定义类的对象,需要自定义类重写哪个/些方法?
List:重写equals()方法
Set:
如果是HashSet或LinkedHashSet,则重写equals()方法和hashCode()方法。
如果是TreeSet,则重写compare(自制排序:Comparable)或compareTo方法(自然排序:Comparator)
七.Map接口
1.Map接口继承结构图
2.Map各个实现类的特点
key和value类似于函数。
类 | 特点 |
---|---|
HashMap | 是Map的主要实现类,是线程不安全的,效率高。可以存储null的key和value |
LinkedHashMap | 可以保证在遍历Map元素时,按照添加的顺序实现遍历。对于频繁的遍历操作,此类的执行效率高于HashMap |
TreeMap | 可以保证按照添加的键值对进行排序,实现排序遍历,此时考虑key的自然排序或者定制排序,而不是value的。底层使用红黑树实现 |
Hashtable | 是Map的古老实现类,是线程安全的,效率低。不能存储null的key和value |
Properties | 常用来处理配置文件。要求key和value都是String类型 |
3.Map键值对的理解
对象 | 特点 |
---|---|
Map中的key | 是无序的、不可重复的。使用Set存储所有的key 。所以key所在的类要重写equals和hashCode方法(以hashMap为例) |
Map中的value | 是无序的、可重复的。使用Collection来存储所有的value。所以value所在的类要重写equals方法 |
一个键值对 | 构成了一个Entry对象。Map中的entry是无序的、不可重复的(和key一致),所以使用Set存储所有的entry |
4.HashMap的底层实现原理
具体的可以看这个博客
JDK7:
在执行完这一条语句后
HashMap map = new HashMap();
也就是实例化以后,底层创建了长度是16的一维数组Entry[] table.
当执行时
map.put(key1,value1);
- (1)首先,会调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法计算之后,得到在Entry数组中的存放位置。如果没有冲突,则直接存。---------------情况一
- (2)如果有冲突,则比较key1和已经存在的一个或多个数据的哈希值。
- 如果key1的哈希值与已经存在在的数据哈希值都不相同,则此键值对添加成功。---------------情况二
- 如果key1的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,则调用key1所在类的equals(key2)方法进行比较。
如果equals返回false,则此键值对添加成功。---------------情况三
如果equals返回true,则使用value1去替换value2。此时的put方法还具有一个修改功能。
补充:
①关于情况二和情况三:此时key1-value1和原来的数据以链表的方式存储。
②在不断的添加过程中,会涉及扩容问题,则默认的扩容方式为:扩容为原来容量的2倍,并将原有的数据复制过来。
jdk8相较于jdk7在底层实现原理的不同:
①在new HashMap()之后,底层没有创建一个长度为16的数组,而是数组是Node数组而非Entry数组
②在首次调用put方法时,底层创建长度为16的数组。(类似于ArrayList )
③jdk7底层结构只有数组+链表。在jdk8中底层结构有数组+链表+红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64,则此索引位置上的所有数据改为使用红黑树存储。(提高查找效率)
5.Map接口常用方法
添加:put(Object key,Object value)
删除:remove(Object key)
修改:put(Object key,Object value)
查询:get(Object key)
长度:size()
遍历:keySet()/values()/entrySet()
没有插入,因为Map是无序的,没法插。
(1)put、putAll、remove、clear(增删改)
Map map = new HashMap();
//put方法
map.put("AA",123);
map.put("BB",456);
map.put(1,999);
map.put("AA","替换后的");
System.out.println(map);//{AA=替换后的, BB=456, 1=999}
//purAll方法
Map map1 = new HashMap();
map1.put(11,11);
map.putAll(map1);
System.out.println(map);//{AA=替换后的, BB=456, 1=999, 11=11}
//remove
map.remove("AA");
System.out.println(map);//{BB=456, 1=999, 11=11}
//clear
map.clear();//与map=null不同
System.out.println(map);//{}
System.out.println(map.size());//0
(2)查
put、get、containsKay、containsValue、isEmpty、equals
Map map = new HashMap();
//put方法
map.put("AA",123);
map.put("BB",456);
map.put(1,999);
//get
System.out.println(map.get("AA"));//123
//containsKay
System.out.println(map.containsKey("AA"));//true
//containsValue
System.out.println(map.containsValue(123));//true
//isEmpty
System.out.println(map.isEmpty());//false
//equals
Map map1 = new HashMap();
map1.put(111,111);
System.out.println(map.equals(map1));//false
Map map2 = new HashMap();
map2.putAll(map1);
System.out.println(map2.equals(map1));//true
(3)遍历(keySet、values、entrySet)
Map没有对应的迭代器,但是可以先得到key的Set集合,然后使用迭代器遍历。也可以得到value的Collection集合,然后使用迭代器遍历。也可以得到所有Entry构成的Set,然后使用迭代器遍历。
Map map = new HashMap();
//put方法
map.put("AA",123);
map.put("BB",456);
map.put(1,999);
//keySet
Set set = map.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
/*
AA
BB
1
*/
//values
Collection values = map.values();
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()){
System.out.println(iterator1.next());
}
/*
123
456
999
*/
//遍历所有的key-value
Set set1 = map.entrySet();
Iterator iterator2 = set1.iterator();
while (iterator2.hasNext()){
Object obj = iterator2.next();
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey() +"-->"+ entry.getValue());
}
/*
AA-->123
BB-->456
1-->999
*/
6.TreeMap
向TreeMap中添加key-value,要求key必须是由同一个类创建的对象。因为要按照key进行排序(自然排序、定制排序),所以key的类要实现Comparable,或者创建Comparator对象。
自然排序
Map map = new TreeMap();
//创建几个User类
User u1 = new User("bb",11);
User u2 = new User("aa",22);
User u3 = new User("dd",2);
//put方法
map.put(u1,123);
map.put(u2,456);
map.put(u3,999);
System.out.println(map);
/*
{User{name='aa', num=22}=456,
User{name='bb', num=11}=123,
User{name='dd', num=2}=999}
*/
定制排序
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return u1.name.compareTo(u2.name);
}else {
throw new RuntimeException("类型不匹配");
}
}
};
Map map = new TreeMap(comparator);
//创建几个User类
User u1 = new User("bb",11);
User u2 = new User("aa",22);
User u3 = new User("dd",2);
//put方法
map.put(u1,123);
map.put(u2,456);
map.put(u3,999);
System.out.println(map);
/*
{User{name='aa', num=22}=456,
User{name='bb', num=11}=123,
User{name='dd', num=2}=999}
*/
7.Properties
常用来处理配置文件。要求key和value都是String类型
直接看使用示例
Properties pros = new Properties();
//加载流对应的文件
FileInputStream fis = new FileInputStream("test.properties") ;
pros.load(fis);
System.out.println(pros.getProperty("name"));
System.out.println(pros.getProperty("password"));
/*
Tom
111222
*/
fis.close();
八.Collections工具类
Collections是一个操作Collection(Set、List)和Map等集合的工具类
常规方法
List list = new ArrayList();
list.add(123);
list.add(-11);
list.add(60);
list.add(200);
System.out.println(list);//[123, -11, 60, 200]
//reverse:逆置
Collections.reverse(list);
System.out.println(list);//[200, 60, -11, 123]
/*
[123, -11, 60, 200]
[200, 60, -11, 123]
*/
//shuffle:对List集合元素进行随机排序
Collections.shuffle(list);
System.out.println(list);//[60, 200, 123, -11]
//sort(List):根据元素的自然排序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println(list);//[-11, 60, 123, 200]
//sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
//swap:交换
Collections.swap(list,0,1);
System.out.println(list);//[60, -11, 123, 200]
//max与min,得到最大值和最小值
//frequency:返回指定集合中指定元素的出现次数
System.out.println(Collections.frequency(list,60));//1
//copy:复制
List dest = Arrays.asList(new Object[list.size()]);
Collections.copy(dest,list);
System.out.println(dest);//[60, -11, 123, 200]
synchronizedxxx()的使用
Collections类中提供了多个synchronizedXxx()方法,该方法可将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
之前说过ArrayList和HashMap这些是线程不安全的,但是我们也不会因此去选择使用Vector或者Hashtable,而是把他们包装成线程安全的。
List list1 = Collections.synchronizedList(list);