今天说一下Set接口下的容器和Map容器,说的不好还请指正,感谢.
1.Set接口
Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素.
Set 接口存储一组唯一,无序的对象,且最多包含一个null元素.
下面介绍Set的两个实现类
1.1 HashSet不一定一致
该类实现了Set接口,存储的数据不允许出现重复元素,且不能保证集合中元素的顺序,即向集合中存储数据的顺序跟在集合中存储数据的顺序 不一定一样.
HashSet的底层是由哈希表(HashTable)实现的
HashSet的特点是查询、增删效率高,但是无序且不可重复
推荐使用HashSet
示例1:
import java.util.HashSet;
import java.util.Set;
public class TestHashSet {
public static void main(String[] args) {
Set<Person>set=new HashSet<Person>();
set.add(new Person("张三",12));
set.add(new Person("李四",18));
set.add(new Person("王五",16));
set.add(new Person("赵六",21));
set.add(new Person("赵六",21));
System.out.println(set);
}
}
class Person {
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
}
打印结果为:
[Person [name=王五, age=16], Person [name=赵六, age=21], Person [name=张三, age=12], Person [name=李四, age=18], Person [name=赵六, age=21]]
我们注意到有两个Person [name=赵六, age=21],这是怎么回事呢?
我们在HashSet中放入数据时,先是根据哈希算法hashcode()方法计算这个数据应该存入哈希数组中的那个位置;确定了数据存入的位置后,再判断这个数据在这个位置上是否重复,调用equals()方法进行判断.如果返回true就表示这个数据重复,不添加;返回false就往里面添加数据.在同一个位置的数据是以链表的形式排列的,当一个位置的数据大于八个时,链表转为红黑树.
这样我们就明白了了,HashSet是根据hashCode()和equals()两个方法确定是否往集合中添加数据的.所以我们对这种两个Person[name=赵六,age=21]的情况只需要重写这两个方法就可以了.
下面是更改之后的Person类:
class Person {
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
再次运行的结果为:
[Person [name=李四, age=18], Person [name=王五, age=16], Person [name=张三, age=12], Person [name=赵六, age=21]]
所以当HashSet中存放的是自定义引用数据类型的变量时.当出现往HashSet中添加数据时会出现重复的情况时,只需重写hashCode()和equals()两个方法即可.
- 如果两个对象的hashCode()值相同,不一定是一个对象,需要进一步比较equals()
- 如果两个对象的hashCode()值不相同,肯定不是一个对象
1.2TreeSet
该类实现了Set接口,可以实现排序等功能,所以就不能使用多态.
TreeSet的底层是由TreeMap实现的,存储形式是红黑树
TreeSet的优点是其中的元素默认升序
应用场景可以是在需要去重,数据有排序且没有索引的的情况
示例2:
import java.util.TreeSet;
public class TestTree1 {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<Integer>();
set.add(12);
set.add(43);
set.add(23);
set.add(2);
set.add(1);
System.out.println(set);
}
}
打印结果为:
[1, 2, 12, 23, 43]
我们可以看到存入的数据是默认升序的.
1.3比较器
来先看一下下面的程序: 示例3:
import java.util.TreeSet;
public class TestTreeSet {
public static void main(String[] args) {
TreeSet<Person>set=new TreeSet<Person>();
set.add(new Person("张三",12));
set.add(new Person("李四",18));
set.add(new Person("王五",16));
set.add(new Person("赵六",21));
System.out.println(set);
}
}
class Person{
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
}
运行之后的情况:
Exception in thread "main" java.lang.ClassCastException: com.shy.test.Person cannot be cast to java.lang.Comparable
有个异常!!!!
我们可以看到最后有个Comparable,这是一个接口.此接口强行对实现它的每个类的对象进行整体排序.
int compareTo(T o) 比较此对象与指定对象的顺序
因为TreeSet是默认对存入其中的数据进行升序,但是我们这个对象又没有实现这个这个接口,所以抛出了这个异常.
但是为什么我们向TreeSet中存入数字没有抛出这个异常呢,这是因为我们在向容器中存入基本数据类型的数据时,程序会自动把这个数据包装成对应的包装类.就像int被包装成了Integer.Integer实现了Comparable接口.
所以当我们向TreeSet中存入自定义引用数据类型的数据时,假若没有实现Comparable接口就会抛出异常.
下面是实现了Comparable接口的Person类:
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Person o) {
return this.age-o.age;
}
}
可以看到我们实现了Comparable接口,重写了其中的comparaTo()方法,这个时候就可以就可以根据age属性进行升序排列.
而且,实现了这个接口之后,还可以实现去重.
示例4:
import java.util.TreeSet;
public class TestTreeSet {
public static void main(String[] args) {
TreeSet<Person>set=new TreeSet<Person>();
set.add(new Person("张三",12));
set.add(new Person("李四",18));
set.add(new Person("王五",16));
set.add(new Person("赵六",21));
set.add(new Person("赵六",21));
System.out.println(set);
}
}
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Person o) {
return this.age-o.age;
}
}
程序运行结果为:
[Person [name=张三, age=12], Person [name=王五, age=16], Person [name=李四, age=18], Person [name=赵六, age=21]]
可以看到实现了升序以及去重的效果.
上面说的是内部比较器,实现Comparable接口,重写comparaTo() 方法.
但这种情况每次修改需要修改源码,不符合设计原则开闭原则:对修改关闭,对扩展开放.
所以下面介绍外部比较器:
使用外部比较器是要实现接口java.util.Comparator的接口,重写compare()方法,可以在方法中自定义比较规则,下面是这个接口中的方法
int compare(T o1, T o2)
比较用来排序的两个参数
看一下外部比较器的使用:
示例5:
import java.util.Comparator;
import java.util.TreeSet;
public class TestTreeSet {
public static void main(String[] args) {
//匿名内部类
TreeSet<Person> set = new TreeSet<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
set.add(new Person("张三", 12));
set.add(new Person("李四", 18));
set.add(new Person("王五", 16));
set.add(new Person("赵六", 21));
set.add(new Person("赵六", 21));
System.out.println(set);
}
}
class Person {
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
}
程序运行结果为:
[Person [name=张三, age=12], Person [name=王五, age=16], Person [name=李四, age=18], Person [name=赵六, age=21]]
可以看到实现了按照age升序排列,且去重了.
因为我们实现这个接口就是为了重写其中的方法,所以为了简化代码,使用了匿名内部类.
所以如果我们在TreeSet中存储自定义引用数据类型的数据时,一定要设置比较器,内部比较器、外部比较器都行.但建议使用外部比较器.
2.Map接口
Map 接口存储一组键值对象,提供key(键)到value(值)的映射.
- key的特点: 无序的,不可重复的 -->所有的key,就是一个set集合
- value的特点: 无序的,可重复 --> Collection的特点
- 一个key对象一个value值,如果想要对应多个,多个value值可以存在在一个数组|容器中
- key相同时候,value会覆盖
2.1HashMap
- HashMap的底层是哈希表的结构;
- 该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步
- HashMap中没有新增功能,可以使用多态
- 因为所有的key值就是一个set集合,所以如果HashMap的key是自定义的引用数据类型,就需要对key的数据的类型重写hashCode()和equals()方法,实现去重,这个时候value会覆盖
- 如果想要根据value值进行去重,我们可以手动使用containsValue()进行判断.
下面看一个HashMap的使用:
示例6:
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
public static void main(String[] args) {
Map<Person, Integer> map = new HashMap<Person, Integer>();
map.put(new Person("张三", 12), 23);
map.put(new Person("李四", 18), 13);
map.put(new Person("王五", 16), 45);
map.put(new Person("赵六", 21), 56);
map.put(new Person("赵六", 21), 56);
System.out.println(map);
}
}
class Person {
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
可以看到,因为存储的自定义引用类型数据,所以重写了hashCode()和equals()方法,实现了去重.
2.2HashMap的遍历----->遍历key值
import java.util.*;
public class Test {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("A", "1");
map.put("B", "2");
map.put("C", "3");
/*
* 第一种: Set<K>
* keySet() 返回此映射中所包含的键的 Set 视图
*/
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= " + key + " and value= " + map.get(key));
}
/*
* 第二种: 迭代器 Iterator<E>
* iterator() 返回在此 set 中的元素上进行迭代的迭代器
*/
System.out.println("通过Map.keySet使用iterator遍历key和value:");
Set<String> set = map.keySet();
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String key = it.next();
System.out.println("key= " + key + " and value= " + map.get(key));
}
/*
* 第三种:推荐,尤其容量大时 Set<Map.Entry<K,V>>
* entrySet() 返回此映射所包含的映射关系的 Set 视图
*/
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
}
}
2.3TreeMap
基于红黑树的 NavigableMap实现.该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法
因为TreeMap同样基于红黑树,所以我们在向其中存储自定义引用类型数据时,要写比较器.
下面看一下TreeMap的使用:
示例7:
import java.util.Comparator;
import java.util.Map; import java.util.TreeMap;
public class TestTreeMap {
public static void main(String[] args) {
Map<Person, Integer> map = new TreeMap<Person, Integer>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
map.put(new Person("张三", 12), 23);
map.put(new Person("李四", 18), 13);
map.put(new Person("王五", 16), 45);
map.put(new Person("赵六", 21), 56);
map.put(new Person("赵六", 21), 56);
System.out.println(map);
}
}
class Person {
private String name;
private int age;
public Person(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 "Person [name=" + name + ", age=" + age + "]";
}
}
程序运行结果为:
{Person [name=张三, age=12]=23, Person [name=王五, age=16]=45, Person [name=李四, age=18]=13, Person [name=赵六, age=21]=56}
实现了去重以及升序.
3.总结
昨天到今天说了六种容器,那么哪种容器适用于哪种情况呢,下面来说一下:
- 如果需要查找以及获取元素的速度快,我们可以使用ArrayList
- 如果需要大量的进行增删操作,我们可以使用LinkedList
- 如果想要容器中的每个数据唯一且无序,我们可以使用HashSet
- 如多想要容器中的每个数据唯一但是要升序或降序,我们可以使用TreeSet
- 如果想要存储键值对数据且键值对无序唯一,我们就可以使用HashMap
- 如果想要存储键值对数据且想让键值对根据其中的元素升序或降序,我们就可以使用TreeMap
- 使用HashMap时,想要线程安全,我们可以使用 HashTable
- 使用ArrayList时,想要线程安全,我们可以使用Vector