第一讲 Set
一 概述
Set:1. 元素存储无下标,所以元素是无序(存入和取出的顺序不一定一致)
2. 元素不可以重复
|--HashSet:底层数据结构是哈希表。线程不同步。 保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true。
|--TreeSet:可以对Set集合中的元素进行排序。默认按照字母的自然排序。底层数据结构是二叉树。保证元素唯一性的依据:compareTo方法return 0。
Set集合没有特有的功能,Set集合的功能和Collection是一致的
二 HashSet
HashSet:线程不安全,存取速度快。
HashSet的原理: HashSet中不能存入重复的值,如果add()存入重复值,返回false,那么HashSet如何保证数据是不是唯一的呢?
如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数
可以通过元素的两个方法,hashCode和equals来完成保证元素唯一性。如果元素的HashCode值相同,才会判断equals是否为true。如果元素的hashCode值不同,不会调用equals。
2. 知道了HashSet原理之后所以我们在存储自定义类的时候必须重写hashCode()和equals()方法
* hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)
* equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储
3. HashSet扩展,如果遇到这样一种情况,既需要保证元素的唯一性,但是同样需要保证元素的存取顺序,那么就可以用HashSet的子类—LinkedHashSet 链表HashSet
例子1:往hashSet集合中存入自定对象 姓名和年龄相同为同一个人,重复元素。要去除重复元素
/*
往hashSet集合中存入自定对象
姓名和年龄相同为同一个人,重复元素。去除重复元素
思路:1、对人描述,将人的一些属性等封装进对象
2、定义一个HashSet容器,存储人对象
3、取出
*/
import java.util.*;
//人描述
class Person
{
private String name;
private int age;
Person(String name,int age)
{
this.name=name;
this.age=age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public boolean equals(Object obj)
{
if(!(obj instanceof Person))
return false;
Person p=(Person)obj;
return this.name.equals(p.name)&&this.age==p.age;
}
public int hashCode()
{
return this.name.hashCode()+this.age;
}
}
class HashSetTest
{
public static void main(String[] args)
{
HashSet h=new HashSet();
h.add(new Person("shenm",10));
h.add(new Person("shenm2",6));
h.add(new Person("shenm1",30));
h.add(new Person("shenm0",10));
h.add(new Person("shenm0",10));
getOut(h);
}
//取出元素
public static void getOut(HashSet h)
{
for (Iterator it=h.iterator(); it.hasNext(); )
{
Person p=(Person)it.next();
sop(p.getName()+"..."+p.getAge());
}
}
//打印
public static void sop(Object obj)
{
System.out.println(obj);
}
}
例子2:使用Scanner从键盘读取一行输入,去掉其中重复字符, 打印出不同的那些字符
Scanner sc = new Scanner(System.in); //创建键盘录入对象
System.out.println("请输入一行字符串:");
String line = sc.nextLine(); //将键盘录入的字符串存储在line中
char[] arr = line.toCharArray(); //将字符串转换成字符数组
LinkedHashSet<Character> hs = new LinkedHashSet<>();
for(char c : arr) { //遍历字符数组
hs.add(c); //将字符数组中的字符添加到集合中
}
for (Character ch : hs) { //遍历集合
System.out.print(ch);
}
三 TreeSet
1. 特点
a) 底层的存储数据结构为二叉树结构
b) 存储在TreeSet集合中的元素会进行排序,我们可以指定一个顺序, 对象存入之后会按照指定的顺序排列
1. 这同时也意味着存入TreeSet中元素必须是同一种类型(如果一个存Integer,一个存String就会报java.lang.ClassCastException异常)
2. 也意味着我们在创建TreeSet的同时,必须为排序指定顺序(下面讲解1.自然顺序2.选择器),如果没有指定那么同样会报异常
小知识点:8种基本数据类型的封装类都默认实现了Comparable接口,可以直接比较
c) 从排序方式我们可以具体知道TreeSet二叉树存储的原理
2. 为TreeSet指定排序顺序
① 第一种排序方式:自然排序
让元素自身具备比较性。元素需要实现Comparable接口,覆盖compareTo方法。这种方式也被称为元素的自然顺序,或者叫做默认顺序。
示例:在TreeSey集合中插入一些学生对象(姓名和年龄),按年龄从小到大进行排序,如果年龄相同按姓名的字典序排序
步骤1 : 定义学生类,继承Comparable接口,重写compareTo()方法
static class Student implements Comparable<Student> { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + "...." + age; } @Override public boolean equals(Object obj) { if(!(obj instanceof Student)) { return false; } Student s = (Student)obj; return this.name.equals(s.name)&&this.age==s.age; } /** * 复写compareTo以便TreeSet集合调用 * 按年龄从小到大排序,如果年龄一样按名字的字典序排列 */ @Override public int compareTo(Student o) { int num = this.age - o .getAge(); return num == 0 ? this.name.compareTo(o.getName()):num; } }
ComPareTo()方法中返回值类型代表在二叉树的左右方向,this代表刚加入的方法,Student o为已经存储在二叉树结构中对象
如果返回值 1. 返回值大于0,this在二叉树的右边 2. 如果返回值小于0,this在二叉树的左边 3. 如果返回值等于0,代表this与Student o一致,直接抛弃不存储
而TreeSet读取的时候根据二叉树的前序遍历分别将元素取出
步骤2: 加入元素,查看结果,分析结果
TreeSet<Student> set = new TreeSet<>();
set.add(new Student("张三", 23));
set.add(new Student("李四", 22));
set.add(new Student("王五", 36));
set.add(new Student("周六", 12));
System.out.println(set);
结果为:[周六....12, 李四....22, 张三....23, 王五....36]
分析结果:略
② 第二种排序方式: 选择器
在集合初始化时,定义一个比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。 格式如下:
TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>() {//将比较器传给TreeSet的构造方法
@Override
public int compare(Integer i1, Integer i2) {
// 比较器逻辑
}
});
当两种排序都存在时,以比较器为主。
上面的例子也可以这样写
TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int num = o1.getAge() -o2.getAge();
return num == 0? o1.getName().compareTo(o2.getName()):num;
}
});
set.add(new Student("张三", 23));
set.add(new Student("李四", 22));
set.add(new Student("王五", 36));
set.add(new Student("周六", 12));
System.out.println(set);
结果:[周六....12, 李四....22, 张三....23, 王五....36]
注意:调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
③ 两种排序方式的区别:
* TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)
* TreeSet如果传入Comparator, 就优先按照Comparator
第四讲 Map
一 概述
1. 简述
① Map<K,V>集合是一个接口,和List集合及Set集合不同的是,它是双列集合,并且可以给对象加上名字,即键(Key),然后将键映射到值的对象
② 一个映射不能包含重复的键,键可以存储null
③ 每个键最多只能映射到一个值
2. HashSet与HashMap
HashSet底层使用HashMap实现的,HashSet的键—值对的键用来存储值,所以能保证唯一,而值是自定义的一个object
3. Map体系(子类)
|--HashMap:底层是哈希表数据结构。允许使用null键null值,该集合是不同步的。JDK1.2,效率高。
|--TreeMap:底层是二叉树数据结构。线程不同步。可以用于给Map集合中的键进行排序。
二 Map集合常用的方法
1. 添加
V put(K key,V value):添加元素。
* 如果键是第一次存储,就直接存储元素,返回null
* 如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值
2. 删除
* void clear():移除所有的键值对元素
* V remove(Object key):根据键删除键值对元素,并把值返回
3. 判断
* boolean containsKey(Object key):判断集合是否包含指定的键
* boolean containsValue(Object value):判断集合是否包含指定的值
* boolean isEmpty():判断集合是否为空
4. 获取
*Collection<V> values():获取集合中所有值的集合
* int size():返回集合中的键值对的个数
* Set<Map.Entry<K,V>> entrySet():
* V get(Object key):根据键获取值
* Set<K> keySet():获取集合中所有键的集合
三 Map集合的两种迭代方式
1. 键找值原理: 将Map集合转成Set集合。再通过Set集合的迭代器取出各个键
① 通过Set<K> keySet():将Map中所以的键存入到Set集合
② Set具备迭代器。所以可以通过迭代方式取出所以键的值,
③ 再通过Map集合的get方法。获取每一个键对应的值。
HashMap<String, Integer> hm = new HashMap<>();
hm.put("张三", 23);
hm.put("李四", 24);
hm.put("王五", 25);
hm.put("赵六", 26);
/*Set<String> keySet = hm.keySet(); //获取集合中所有的键
Iterator<String> it = keySet.iterator(); //获取迭代器
while(it.hasNext()) { //判断单列集合中是否有元素
tring key = it.next(); //获取集合中的每一个元素,其实就是双列集合中的键
Integer value = hm.get(key); //根据键获取值
System.out.println(key + "=" + value); //打印键值对
}*/
for(String key : hm.keySet()) { //增强for循环迭代双列集合第一种方式
System.out.println(key + "=" + hm.get(key));
}
2. 键值对对象(Map.Entry<String, Integer>)找键和值思路
① 获取所有键值对对象的集合 hm.entrySet()
② 遍历键值对对象的集合,获取到每一个键值对对象 Entry<String, Integer> en = it.next();
③ 根据键值对对象找键和值 String key = en.getKey(); Integer value = en.getValue();
其实,Entry也是一个接口,它是Map接口中的一个内部接口。
- nterface Map
- {
- public static interface Entry
- {
- public abstract Object getKey();
- public abstract Object getValue();
- }
- }
代码实现:
HashMap<String, Integer> hm = new HashMap<>();
hm.put("张三", 23);
hm.put("李四", 24);
hm.put("王五", 25);
hm.put("赵六", 26);
/*Set<Map.Entry<String, Integer>> entrySet = hm.entrySet(); //获取所有的键值对象的集合
Iterator<Entry<String, Integer>> it = entrySet.iterator();//获取迭代器
while(it.hasNext()) {
Entry<String, Integer> en = it.next(); //获取键值对对象
String key = en.getKey(); //根据键值对对象获取键
Integer value = en.getValue(); //根据键值对对象获取值
System.out.println(key + "=" + value);
}*/
for(Entry<String,Integer> en : hm.entrySet()) {
ystem.out.println(en.getKey() + "=" + en.getValue());
}
关于Map.Entry:
Map是一个接口,其实,Entry也是一个接口,它是Map的子接口中的一个内部接口,就相当于是类中有内部类一样。为何要定义在其内部呢?
原因:a、Map集合中村的是映射关系这样的两个数据,是先有Map这个集合,才可有映射关系的存在,而且此类关系是集合的内部事务。
b、并且这个映射关系可以直接访问Map集合中的内部成员,所以定义在内部。
因为HashMap和TreeMap与Set接口下的HashSet和TreeSet一样(只是将Map键值对中的键当作Set中的值),就不做介绍了,下面来看几个例子
四 例子解析
何时使用Map集合:当量数据之间存在着映射关系的时候,就应该想到使用Map集合。
1. 统计字符串中每个字符出现的次数
String str = "aaaabbbbcccccddddd"; char[] arr = str.toCharArray(); HashMap<Character, Integer> hm = new HashMap<>(); for(char c: arr){ // 循环遍历数组,将字符出现情况传入HashMap中 if(!hm.containsKey(c)){ hm.put(c, 1); }else{ hm.put(c, hm.get(c)+1); } } for (Character key : hm.keySet()) { //遍历双列集合 System.out.println(key + "=" + hm.get(key)); }
2. HashMap和Hashtable的区别
共同点:底层都是采用Hash算法实现,都是双列集合
不同点: 1.HashTable线程安全,需要加同步判断锁,所以效率低,HashMap是JDK1.2版本出现的,是线程不安全的,效率高
2.Hashtable不可以存储null键和null值,HashMap可以存储null键和null值
3. Collections工具类
与Arrays数组工具类类似,集合也由有自己的工具类—Collections,包含一些集合的方法
常用方法:
① public static <T> void sort(List<T> list) 对List集合进行排序
② public static <T> int binarySearch(List<?> list,T key) List集合二分法查找
③ public static <T> T max(Collection<?> coll) 集合中查找最大值
④ public static void reverse(List<?> list) 对List集合进行反转
⑤ public static void shuffle(List<?> list) 对集合进行随机排序
4. 模拟斗地主洗牌和发牌
import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.TreeSet; /** * 用Map集合和List集合模拟斗地主洗牌和发牌 * @author Administrator * */ public class doudizhu { /* * 包含四个步骤 * 1. 构建一副牌 * 2. 洗牌 * 3. 发牌 * 4. 看牌 */ public static void main(String[] args){ // 1. 构建一副牌 String[] num = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"}; String[] color = {"方片","梅花","红桃","黑桃"}; HashMap<Integer,String> hm = new HashMap<>(); // 代表每张牌,存储索引和牌 ArrayList<Integer> list = new ArrayList<>(); // 存储索引,每一个索引对应HashMap中的一张牌 int index = 0 ; for(String s1:num){ for(String s2:color){ hm.put(index, s2.concat(s1)); //将索引和扑克牌添加到HashMap中 list.add(index); //添加索引 index++; } } hm.put(index, "小王"); list.add(index++); hm.put(index, "大王"); list.add(index); // 2. 洗牌 Collections.shuffle(list); // 3. 发牌(三个人和三张的底牌) TreeSet<Integer> zhangsan = new TreeSet<>(); // 用TreeSet(有序)存储张三的牌 TreeSet<Integer> lisi = new TreeSet<>(); // 用TreeSet(有序)存储李四的牌 TreeSet<Integer> wangwu = new TreeSet<>(); // 用TreeSet(有序)存储王五的牌 TreeSet<Integer> dipai = new TreeSet<>(); // 用TreeSet(有序)存储底牌 for(int i=0 ;i<list.size();i++){ // 开始发牌 if(i >= list.size() - 3) { dipai.add(list.get(i)); //将list集合中的索引添加到TreeSet集合中会自动排序 }else if(i % 3 == 0) { zhangsan.add(list.get(i)); }else if(i % 3 == 1) { lisi.add(list.get(i)); }else { wangwu.add(list.get(i)); } } // 4 . 看牌 lookPoker("张三", zhangsan, hm); lookPoker("李四", lisi, hm); lookPoker("王五", wangwu, hm); lookPoker("底牌", dipai, hm); } public static void lookPoker(String name,TreeSet<Integer> ts,HashMap<Integer, String> hm) { System.out.print(name + "的牌是:"); for (Integer index : ts) { System.out.print(hm.get(index) + " "); } System.out.println(); } }