5.Set接口
学习Collection接口时,记得Collection中可以存放重复元素,也可以不存放重复元素,那么我们知道List中是可以存放重复元素的。那么不重复元素给哪里存放呢?那就是Collection接口中的Set集合中的元素就是不重复的。
Set接口介绍
查阅Set集合的API介绍。
Set:不包含重复元素的集合,不保证顺序。而且方法和Collection一致。Set
集合取出元素的方式只有一种:迭代器。
Set集合有多个子类,这里我们介绍其中的HashSet、TreeSet和
LinkedHashSet这三个集合。
HashSet:哈希表结构,不同步,保证元素唯一性的方式依赖于:
hashCode(),equals()方法。查询速度快。
哈希表
什么是哈希表呢?
哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组中存放时,那么会根据这些对象的特有数据结合相应的算法,算法这个对象在数组中的位置,然后把这个对象存放在数组中。而这样的数组就称为哈希数组,即就是哈希表。
当给哈希表中存放元素时,需要根据元素的特有数据结合相应的算法,这个算法其实就是Object中的hashCode方法。由于任何对象都是Object类的子类,所以任何对象有拥有这个方法。即就是在给哈希表中存放对象时,会调用对象的hashCode方法,算出对象在表中的存放位置,这里需要注意,如果两个对象hashCode方法算出结果一样,这样现象称为哈希冲突,这时会调用对象的equals方法,比较这两个对象是不是同一个对象,如果equals方法返回的是true,那么就不会把第二个对象存放在哈希表中,如果返回的是false,就会把这个值存放在哈希表中。
总结:保证元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
HashSet存储自定义对象
给HashSet中存放自定义对象时,需要复写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。
创建自定义对象Student
public class Student {
private String name;
private int age;
public Student(String name, int age) {
super();
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 "Student [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 instanceof Student)){
throw new ClassCastException("类型错误");
}
Student other = (Student) obj;
return this.age == other.age && this.name.equals(other.name);
}
}
创建HashSet集合,存储Student对象。
public class HashSetDemo {
public static void main(String[] args) {
//创建HashSet对象
HashSet hs = new HashSet();
//给集合中添加自定义对象
hs.add(new Student("zhangsan",21));
hs.add(new Student("lisi",22));
hs.add(new Student("wangwu",23));
hs.add(new Student("zhangsan",21));
//取出集合中的每个元素
Iterator it = hs.iterator();
while(it.hasNext()){
Student s = (Student)it.next();
System.out.println(s);
}
}
}
输出结果:
Student [name=lisi, age=22]
Student [name=zhangsan, age=21]
Student [name=wangwu, age=23]
TreeSet集合和存储自定义对象
阅读TreeSet集合的API,使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。
阅读完之后不明白说的啥意思,其实TreeSet集合是可以给对象进行排序的。当存放进去的对象,会根据对象的自身的特点进行自然顺序的排序。因此这里需要注意的当我们给TreeSet集合中存放自定义对象时,一定要保证对象自身具备比较功能,如何才能让对象自身具备比较功能呢?
当需要一个对象自身具备功能时,只需要这个对象实现Comparable接口,并实现其中的compareTo方法即可。
创建学生类,并实现Comparable接口,复写其中的compareTo方法
public class Student implements Comparable{
private String name;
private int age;
public Student(String name, int age) {
super();
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 "Student [name=" + name + ", age=" + age + "]";
}
/*
* 重写compareTo方法,建立学生的自然排序(对象的默认排序方式)。按照学
生年龄排序。
*/
@Override
public int compareTo(Object o) {
if(!(o instanceof Student)){
throw new ClassCastException();
}
Student stu = (Student)o;
// 注意:在比较时,必须明确主次。主要条件相同,继续比较次要条件。
int temp = this.age - stu.age;
return temp==0?this.name.compareTo(stu.name):temp;
}
}
创建TreeSet集合对象,存储Student对象。
public class TreeSetDemo {
public static void main(String[] args) {
//创建TreeSet对象
TreeSet hs = new TreeSet();
//给集合中添加自定义对象
hs.add(new Student("zhangsan",21));
hs.add(new Student("lisi",22));
hs.add(new Student("wangwu",23));
hs.add(new Student("zhangsan",21));
hs.add(new Student("zhaoliu",21));
//取出集合中的每个元素
Iterator it = hs.iterator();
while(it.hasNext()){
Student s = (Student)it.next();
System.out.println(s);
}
}
}
TreeSet集合总结
TreeSet:可以对Set集合中的元素进行排序。使用的是二叉树结构。如何保证元素唯一性的呢?使用的对象比较方法的结果是否为0,是0,视为相同元素不存。元素自身具备自然排序,其实就是实现了Comparable接口重写了compareTo方法。如果元素自身不具备自然排序,或者具备的自然排序不是所需要的,这时怎么办呢?
继续查看TreeSet集合说明,发现还有一个叫做Comparator的接口,并且TreeSet集合的接口允许我们给其传递这样一个接口的子类对象。
其实就是在创建TreeSet集合时,在构造函数中指定具体的比较方式。需要定义一个类实现Comparator接口,重写compare方法。
到此为止:再往集合中存储对象时,通常该对象都需要覆hashCode,equals,同时实现Comparale接口,建立对象的自然排序。通常还有一个方法也会复写toString();
6.比较器
我们知道String是一个最终的类,它自身也具备了比较功能,可是它是按照字符串的自然顺序进行比较,但是我们如果想按照字符其他特点进行比较的话,这时就无法去休息String类中的比较功能,那么怎么呢?这时我们可以使用比较器来完成。
比较器Comparator
这里假设学生类已经具备自身的比较功能,但不是我们想要的,这时我们可以采用比较器方式来实现排序。
public class ComparatorByName extends Object implements
Comparator {
@Override
public int compare(Object o1, Object o2) {
//1,因为要比较的是学生对象的姓名。所以向下转型成Student对象。
Student s1 = (Student)o1;
Student s2 = (Student)o2;
//先比较姓名。
int temp = s1.getName().compareTo(s2.getName());
//如果姓名相同,再比较年龄。
return temp==0? s1.getAge()-s2.getAge():temp;
}
}
public class TreeSetDemo2 {
public static void main(String[] args) {
//在创建TreeSet集合对象时明确比较器。
Set set = new TreeSet(new ComparatorByName());
/*
* 想要按照学生的姓名排序,说明学生中的自然排序不是所需要的。
* 这时只能使用比较器。ComparatorByName。
*/
set.add(new Student("lisi6",21));
set.add(new Student("lisi8",22));
set.add(new Student("lisi5",25));
set.add(new Student("lisi3",23));
set.add(new Student("lisi7",20));
for (Iterator it = set.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
}
比较器练习
练习:要对字符串进行长度(由短到长)排序。
思路:
1,字符串之所以可以排序,因为是已经实现Comparable接口重写
compareTo方法。建立了字符串的自然排序。
2,但是自然排序不是需求中所需要的。咋办?只能使用比较器。需要自定
义一个比较器。
public class ComparatorByLength implements Comparator {
@Override
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
int temp = s1.length() - s2.length();
return temp==0?s1.compareTo(s2):temp;
}
}
public class TreeSetTest {
public static void main(String[] args) {
Set set = new TreeSet(new ComparatorByLength());
set.add("abc");
set.add("haha");
set.add("xixi");
set.add("z");
set.add("hiahia");
for (Iterator it = set.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
}
LinkedHashSet介绍
我们知道HashSet保证元素唯一,并且查询速度很快,可是元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?
在HashSet下面有一个子类LinkedHashSet,它是链表和哈希表组合的一个数据存储结构。
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
for (Iterator it = set.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
}
输出结果:
bbb
aaa
abc
bbc
LinkedHashSet集合保证元素的存入和取出的顺序。
7.集合的其他操作
foreach循环
foreach是JDK1.5以后出来一个高级for循环,专门用来遍历数组和集合的。
格式:
for(元素的数据类型 变量 : Collection集合or数组){}
用于遍历Collection和数组。通常只能遍历元素,不要在遍历的过程中做对集合元素的操作。
List list = new ArrayList();
list.add("itcast1");
list.add("itcast2");
list.add("itcast3");
list.add("itcast4");
for(Object obj : list){//简化。
System.out.println(obj);
}
foreach和老式的for循环有什么区别?
注意:新for循环必须有被遍历的目标。目标只能是Collection或者是数组。其实就是实现了Iterable接口都可以使用foreach遍历。
建议:遍历数组时,如果仅为遍历,可以使用增强for如果要对数组的元素进行 操作,使用老式for循环可以通过角标操作。
枚举Enumeration介绍
在集合框架还为出现之前,当时就有Vector集合存在,那么当时是如何遍历的呢?
那个时候也有专门用来遍历集合的迭代器,这个迭代器就是Enumeration接口。关于Enumeration的使用作为了解即可。在Enumeration接口的API描述中注:此接口的功能与Iterator 接口的功能是重复的。此外,Iterator 接口添加了一个可选的移除操作,并使用较短的方法名。新的实现应该优先考虑使用 Iterator 接口而不是 Enumeration 接口。
因此大家以后遍历集合使用Iterator和foreach来遍历。
Vector v = new Vector();
v.addElement("haha1");
v.addElement("haha2");
v.addElement("haha3");
v.addElement("haha4");
for(Enumeration en = v.elements(); en.hasMoreElements(); ){
System.out.println("enumeration:"+en.nextElement());
}
查阅集合的小技巧
到目前为止,我们已经学习了集合中的大部分常用的集合,这时我们需要总结下关于集合的查阅规律:
看集合对象的小技巧:
集合分体系:List和Set
子类对象的后缀名是所属体系,前缀名是数据结构名称。
List:新出的子类都是以List结尾的,通常都是非同步的。
ArrayList :看到array,就知道数组,查询速度快。
LinkedList:看到link,就知道链表,增删速度快。
Set:
HashSet:看到hash,就知道哈希表,查询速度更快,并想到元素唯一,通过hashCode(),equals方法保证唯一性。
TreeSet:看到tree,就知道二叉树,可以排序,排序想到Comparable-
compareTo Comparator–compare方法。
8.集合工具类
Collections工具类使用
在集合框架中给我们提供相应的工具类,这个工具类存放在java.util包中,类名是Collections,它是专门用来操作集合的工具类,并且其中的方法都是静态的,不需要创建对象就可以使用。
- 获取Collection最值。
public static <T extends Object & Comparable<? super T>> T
max(Collection<? extends T> coll) 获取集合中最大元素(按照默认比较方式)
public static T max(Collection<? extends T> coll,Comparator<? super T>
comp)获取集合中最大元素(按照指定比较器比较方式)
public static <T extends Object & Comparable<? super T>> T min(Collection<?extends T> coll)获取集合中最小元素(按照默认比较方式)
public static T min(Collection<? extends T> coll,Comparator<? super T>
comp)获取集合中最小元素(按照指定比较器比较方式) - 对List集合排序,也可以二分查找。
public static <T extends Comparable<? super T>> void sort(List list)对集合
排序(按照默认比较方式)
public static void sort(List list,Comparator<? super T> c)对集合排序
(按照指定比较器比较方式)
public static int binarySearch(List<? extends Comparable<? super T>>
list,T key)二分法查找 - 对排序逆序。
public static Comparator reverseOrder() 对集合原有排序强行逆转 - 可以将非同步的集合转成同步的集合。
public static List synchronizedList(List list)将非同步List集合转成同步List集合 - 获取任意集合的枚举对象
public static Enumeration enumeration(Collection c)
这些方法只需要练习,掌握其使用规律即可。它们都是用来辅助我们操作集合的。
Arrays工具类-集合和数组的互转
Arrays:用于操作数组的工具类。类中定义的都是静态工具方法
1,对数组排序。sort
2,二分查找。binarySearch
3,数组复制。copyOf
4,对两个数组进行元素的比较,判断两个数组是否相同。equals
5,将数组转成集合。public static List asList(T… a)
为什么将数组转成集合?就为了使用集合的方法操作数组中的元素。但是不要使用增删等改变长度的方法。
add remove 发生UnsupportedOperationException,把数组转成集合后,集合的长度是固定,无法增删。
注意一下细节问题:
int[] arr = {34,21,67};
List list = Arrays.asList(arr);
System.out.println(list);
结果:[[I@18b3364]
如果数组中存储的是基本数据类型,那么转成集合,数组对象会作为集合中的元素存在。
数组中元素时引用数据类型时,转成,数组元素会作为集合元素存在。
Integer[] arr = {34,21,67};
List list = Arrays.asList(arr);
System.out.println(list);
结果:[34, 21, 67]
集合转成数组:使用Collections中的toArray方法。
Collection<String> c = new ArrayList<String>();
c.add("haha1");
c.add("haha2");
c.add("haha3");
c.add("haha4");
String[] str_arr = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(str_arr));
如果传递的数组的长度小于集合的长度,会创建一个同类型的数组长度为集合的长度。
如果传递的数组的长度大于了集合的长度,就会使用这个数组,没有存储元素的位置为null。
长度最好直接定义为和集合长度一致。
为什么要把集合转成数组呢?就是为了限定其对集合的增删操作。
9.Map集合
Map集合的特点
大家回顾我们在前面学习时说当数据多了使用数组来存放,对象多了使用Collection中的集合来存放。前面在学习数组还是给大家讲过,如果我们的数据和数组角标有一定的对应关系,是可以把数据存放在数组中,通过数组的角标来获取对应的数据,把这种方式称为查表法。
可是当我们的对象与对象之间有了对应的关系,我们需要把这样具有对应关系的一对数据存放起来怎么做呢?采用数组只能存放具有简单对应关系的数据,不太合适。采用Collection集合,可是只能存放一个对象,无法维护这种关系。怎么做呢?
为了解决这样的问题,Java中我们提供了相应的其他容器来解决,这个容器就是Map集合。打开API查询Map的描述。将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
阅读完Map集合的介绍之后发现,原来Map集合就是用来存放具有对应关系的数据的。即就是key对应value这样的关系,并且要求key不能重复。
public class MapDemo {
public static void main(String[] args) {
//创建Map对象
Map<String, String> map = new HashMap<String,String>();
//给map中添加元素
map.put("星期一", "Monday");
map.put("星期日", "Sunday");
//当给Map中添加元素,会返回key对应的原来的value值,若key没有
对象的值,返回null
System.out.println(map.put("星期一", "Mon"));
//根据指定的key获取对应的value
String en = map.get("星期日");
System.out.println(en);
//根据key删除元素,会返回key对应的value值
String value = map.remove("星期日");
System.out.println(value);
}
}
keySet()方法演示
如何获取Map中的所有key呢?由于Map中的所有key都是不重复的,所以获取到Map中的所有key应该会存放在Set集合中。那么查询Map的API发现,有个方法keySet。
public class MapDemo {
public static void main(String[] args) {
//创建Map对象
Map<String, String> map = new HashMap<String,String>();
//给map中添加元素
map.put("星期一", "Monday");
map.put("星期日", "Sunday");
//获取Map中的所有key
Set<String> keySet = map.keySet();
//遍历存放所有key的Set集合
Iterator<String> it =keySet.iterator();
while(it.hasNext()){
//得到每一个key
String key = it.next();
//通过key获取对应的value
String value = map.get(key);
System.out.println(key+"="+value);
}
}
}
entrySet()方法演示
继续查阅Map的API,发现还有一个方法是entrySet,这个方法的描述是得到所有key和value的映射关系,这是啥意思呢?假设Map中存放一对一对的夫妻,那么entrySet获取到是每一对夫妻这种夫妻关系。
public class MapDemo {
public static void main(String[] args) {
//创建Map对象
Map<String, String> map = new HashMap<String,String>();
//给map中添加元素
map.put("星期一", "Monday");
map.put("星期日", "Sunday");
//获取Map中的所有key与value的对应关系
Set<Entry<String,String>> entrySet = map.entrySet();
//遍历Set集合
Iterator<Entry<String,String>> it =entrySet.iterator();
while(it.hasNext()){
//得到每一对对应关系
Entry<String,String> entry = it.next();
//通过每一对对应关系获取对应的key
String key = entry.getKey();
//通过每一对对应关系获取对应的value
String value = entry.getValue();
System.out.println(key+"="+value);
}
}
}
注意:Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。
通过Map集合中的values方法,可以获取到Map中的所有value。
Map常见子类的特点
Map有多个子类,这里我们主要讲解常用的HashMap和TreeMap集合。
Hashtable:数据结构:哈希表。是同步的,不允许null作为键和值。被hashMap替代。
Hashtable下有子类Properties:属性集,键和值都是字符串,而且可以结合流进行键值的操作。等到了IO流,你会更清楚。
HashMap:数据结构:哈希表。不是同步的,允许null作为键和值。
HashMap下有个子类LinkedHashMap基于链表+哈希表。可以保证map集合有序(存入和取出的顺序一致)。
TreeMap:数据结构:二叉树。不是同步的。可以对map集合中的键进行排序。
HashMap存储自定义键值
练习一:学生对象(姓名,年龄)都有自己的归属地,既然有对应关系。将学生对象和归属地存储到map集合中。注意:同姓名同年龄视为重复的键。
public class HashMapTest {
public static void main(String[] args) {
//1,创建hashmap集合对象。
Map<Student,String> map = new HashMap<Student,String>();
//2,添加元素。
map.put(new Student("lisi",28), "上海");
map.put(new Student("wangwu",22), "北京");
map.put(new Student("zhaoliu",24), "成都");
map.put(new Student("zhouqi",25), "广州");
map.put(new Student("wangwu",22), "南京");
//3,取出元素。keySet entrySet
// Set<Student> keySet = map.keySet();
// for(Student key : keySet){}
for(Student key : map.keySet()){
String value = map.get(key);
System.out.println(key.toString()+"....."+value);
}
}
}
注意:当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
如果要保证map中存放的key和取出的顺序一致,可以使用LinkedHashMap集合来存放。
TreeMap存储自定义键值
练习二: 学生对象(姓名,年龄)都有自己的归属地,既然有对应关系。 将学生对象和归属地存储到map集合中。
注意:同姓名同年龄视为重复的键。 按照学生的年龄进行从小到大的排序。
TreeMap。
public class TreeMapTest {
public static void main(String[] args) {
// 1,创建TreeMap集合对象。
Map<Student, String> map = new TreeMap<Student, String>();
// 2,添加元素。
map.put(new Student("lisi", 28), "上海");
map.put(new Student("wangwu", 22), "北京");
map.put(new Student("zhaoliu", 24), "成都");
map.put(new Student("zhouqi", 25), "广州");
map.put(new Student("wangwu", 22), "南京");
//3,取出所有元素,entrySet()
for(Map.Entry<Student, String> me : map.entrySet()){
Student key = me.getKey();
String value = me.getValue();
System.out.println(key+"::"+value);
}
}
}
注意:给TreeMap存放自定义对象,自定义对象作为key进行排序时,自定义对象必须具备比较功能,即实现Comparable接口。
如果需要特定方式进行比较,我们也可以给TreeMap集合传递自定义的比较器进行比较。
Map练习-字母出现次数
练习:
“werertrtyuifgkiryuiop”,获取字符串中每一个字母出现的次数。要求返回结果个格式
是 a(1)b(2)d(4)…;
思路:
1,获取到字符串中的每一个字母。
2,用字母取查表,如果查到了该字母对应的次数,就将这个次数+1后重新存回表中。
如果没有查到呢?将该字母和1存到表中。
3,每一字母都查完表后,表中记录的就是所有字母出现的次数。
字母和次数之间存在对应关系,而且字母是唯一性的,所以可以使用map集合做表进行查询。通过结果发现 字母有顺序的,所以可以通过map集合中的treemap作为表。
public class MapTest {
public static void main(String[] args) {
String str = "awaa+acr=ebarct,btydui[efgkiryuiop";
str = getCharCount(str);
System.out.println(str);
}
/*
* 获取字符串中的字母出现次数。
*/
public static String getCharCount(String str) {
//1,将字符串转成字符数组。
char[] chs = str.toCharArray();
//2,定义表。treemap.
TreeMap<Character, Integer> map = new TreeMap<Character,
Integer>();
//3,遍历字符数组。
for (int i = 0; i < chs.length; i++) {
//判断必须是字母。
if(!(chs[i]>='a' && chs[i]<='z' || chs[i]>='A' && chs[i]<='Z')){
continue;
}
//4,将遍历到的字母作为键去查map这个表。获取对应的次数。
Integer value = map.get(chs[i]);
//5,有可能要查询的字母在表中不存在对应的次数,需要判断。
//如果返回是null,说明字母没有对应的次数。就将这个字母和1存储到表
中。
if(value == null){
//将字母和1存储。
map.put(chs[i],1);
}else{
//否则,说明有对应的次数对次数自增。将字母和新的次数存储到表
中。
value++;
map.put(chs[i],value);
}
}
return mapToString(map);
}
/*
* 将map集合中的键值转成 格式是 a(1)b(2)d(4)......
* map中有很多数据,无论是多少个,什么类型,最终都变成字符串。
* StringBuffer 这个容器就符合这个需求。如果是单线程,建议使用
StringBuilder。
*
*/
private static String mapToString(Map<Character, Integer> map) {
//1,明确容器。
StringBuilder sb = new StringBuilder();
//2,遍历map集合。
for(Character key : map.keySet()){
Integer value = map.get(key);
sb.append(key+"("+value+")");
}
return sb.toString();
}
}