第十九章 Set、泛型和Map
1. 作业回顾
1.从键盘随机输入10个整数,保存到List中,并按照倒序排序,从大到小的顺序显示出来。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;
public class Day18HomeWork {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
Scanner s = new Scanner(System.in);
for (int i = 0; i < 10; i++) {
System.out.println("请输入第" + (i+1) +"个整数:");
list.add(s.nextInt());
}
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.intValue() - o1.intValue();
}
});
for (Integer i : list) {
System.out.print(i + ", ");
}
s.close();
}
}
2. LinkedHashSet
LinkedHashSet是HashSet的子类,并且其操作和HashSet相同。
LinkedHashSet不允许重复的元素,但是保留插入元素的顺序。
当对LinkedHashSet进行迭代时,它会按照元素的添加顺序来返回。
import java.util.LinkedHashSet;
import java.util.Set;
public class Day1902 {
public static void main(String[] args) {
// Set<String> set = new HashSet<String>();
Set<String> set = new LinkedHashSet<String>();//保留添加元素的顺序
set.add("张三");
set.add("李四");
set.add("王五");
for (String str : set) {
System.out.println(str);
}
}
}
3. TreeSet
TreeSet实现Set接口和SortedSet接口,其操作和HashSet相同。
TreeSet不允许重复元素,但是它将元素按照顺序来存储,此顺序不是插入元素的顺序,而是元素的大小顺序。当对TreeSet进行迭代时,它将按照元素从小到大的顺序来返回。
import java.util.TreeSet;
public class Day1903 {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<Integer>();
set.add(10);
set.add(20);
set.add(30);
set.add(15);
set.add(5);
for (Integer i : set) {
System.out.println(i);
}
System.out.println();
set.add(12);
for (Integer i : set) {
System.out.println(i);
}
}
}
TreeSet对元素进行排序时需要知道排序规则,有两种方式可以解决:
1,元素实现Comparable接口。
2,创建TreeSet时提供一个比较器。
import java.util.Comparator;
import java.util.TreeSet;
public class Day1903 {
public static void main(String[] args) {
//传递一个比较器,从大到小排序
TreeSet<Integer> set = new TreeSet<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.intValue() - o1.intValue();
}
});
set.add(10);
set.add(20);
set.add(30);
set.add(15);
set.add(5);
for (Integer i : set) {
System.out.println(i);
}
System.out.println();
set.add(12);
for (Integer i : set) {
System.out.println(i);
}
}
}
4. 泛型
使用泛型创建的类和方法可以支持不同类型的数据。
import java.util.ArrayList;
import java.util.List;
//自定义的有序列表
class Mylist1{
//Object表示values可以加入任何类型的元素
private List<Object> values = new ArrayList<Object>();
public void add(String str) {
values.add(str);
}
public void remove(String str) {
values.remove(str);
}
}
//自定义的有序列表
class Mylist2{
//Object表示values可以加入任何类型的元素
private List<Object> values = new ArrayList<Object>();
public void add(Integer i) {
values.add(i);
}
public void remove(Integer i) {
values.remove(i);
}
}
//E是泛型参数
class Mylist3<E>{
// private List<E> values = new ArrayList<E>();
private List<Object> values = new ArrayList<Object>();
public void add(E i) {
values.add(i);
}
public void remove(E i) {
values.remove(i);
}
public E get(int index) {
return (E) values.get(index);
}
}
public class Day1904 {
public static void main(String[] args) {
//泛型的类在使用前需要传递泛型参数
Mylist3<String> list1 = new Mylist3<String>();
list1.add("a");
list1.add("b");
list1.add("c");
String str = list1.get(1);
System.out.println(str);//b
Mylist3<Integer> list2 = new Mylist3<Integer>();
list2.add(1);
list2.add(2);
list2.add(3);
Integer i = list2.get(1);
System.out.println(i);//2
}
}
在类名后的<>中声明的T代表任意类型,还可以使用M,K等等,但是一般都使用T(type),在创建类的对象时,使用具体的类型来代替T。
T可以作为类的成员变量,方法的参数和返回值的类型。
4.1泛型限定
在上面的例子中,T代表此类可以接受任何类型,可以使用泛型限定约束此类能接受的类型。
import java.util.ArrayList;
import java.util.List;
//E是泛型参数
//适用泛型类MyList4时,只能使用number的子类作为泛型参数
//使用E extends Number,表示此泛型只能使用Number的子类作为泛型参数
class Mylist4<E extends Number>{
// private List<E> values = new ArrayList<E>();
private List<Object> values = new ArrayList<Object>();
public void add(E i) {
values.add(i);
}
public void remove(E i) {
values.remove(i);
}
public E get(int index) {
return (E) values.get(index);
}
}
public class Day1905 {
public static void main(String[] args) {
//不能接受String作为泛型参数
// Mylist4<String> list1 = new Mylist4<String>();//编译错误
Mylist4<Integer> list2 = new Mylist4<Integer>();
Mylist4<Float> list3 = new Mylist4<Float>();
Mylist4<Byte> list4 = new Mylist4<Byte>();
}
}
4.2泛型通配符
收编
import java.util.ArrayList;
import java.util.List;
public class Day1906 {
public static void main(String[] args) {
List<Number> list = new ArrayList<Number>();
List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
list1.add(2);
list1.add(3);
List<Double> list2 = new ArrayList<Double>();
list2.add(1.0);
list2.add(2.0);
list2.add(3.0);
List<String> list3 = new ArrayList<String>();
list3.add("a");
list3.add("b");
list3.add("c");
//将list1中的所有元素加入到list中
list.addAll(list1);
//将list2中的所有元素加入到list中
list.addAll(list2);
//将list3中的所有元素加入到list中
// list.addAll(list3);//编译错误
for (Number str : list) {
System.out.println(str);
}
}
}
4.3创建泛型方法
import java.util.Date;
public class Day1907 {
//泛型方法,<T>是泛型参数声明
public static <T> void f1(T t) {
System.out.println(t);
}
//泛型方法,<T>是泛型参数声明,返回值类型是T
public static <T> T f2(T t) {
System.out.println(t);
return t;
}
//泛型方法,<T extends Number>是泛型参数声明,返回值类型是T
public static <T extends Number> void f3(T t) {
System.out.println(t);
}
public static void main(String[] args) {
f1(1);
f1("a");
f1(new Date());
System.out.println(f2('a'));
f3(1.0);
// f3("abc");//编译错误
}
}
5. Map接口
Map代表了一个映射表,用于保存键值对。map不能包含重复的key,但是value可以重复。每个key只能对应一个value。
Map示意图:
Map接口API:
import java.util.Collection;
import java.util.Set;
public interface Map<K,V> {
//获取映射表中键值对的数量
int size();
//判断映射表是否为空
boolean isEmpty();
//判断映射表是否包含指定的key
boolean containsKey(Object key);
//判断映射表是否包含指定的value
boolean containsValue(Object value);
//通过key获取value
V get(Object key);
//put不仅可以添加,还可以修改,如果是修改,那么会返回旧value
V put(K key, V value);
//删除指定key所属的Entry
V remove(Object key);
//清空映射表
void clear();
//将全部的key作为一个set集合返回
Set<K> keySet();
//将全部的value作为一个collection集合返回
Collection<V> values();
//将全部的entry作为一个set集合返回
Set<Map.Entry<K, V>> entrySet();
}
6. HashMap
6.1HashMap介绍
HashMap实现了Map接口,HashMap是无序的。
它不保存元素的插入顺序,在内部也不保证元素按照大小顺序排列。
import java.util.HashMap;
import java.util.Map;
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 + "]";
}
}
public class Day1908 {
public static void main(String[] args) {
//使用String作为key的类型,Student作为value的类型
Map<String,Student> map = new HashMap<String, Student>();
//放入三个entry
map.put("20191101", new Student("张三", 20));
map.put("20191102", new Student("李四", 21));
map.put("20191103", new Student("王五", 22));
//通过键获取对应的值
System.out.println(map.get("20191102"));
//如果key存在,新的value将会替代旧的value
map.put("20191102", new Student("马六", 22));//Student [name=李四, age=21]
//通过键获取对应的值
System.out.println(map.get("20191102"));//Student [name=马六, age=22]
System.out.println(map);//HashMap重写了toString方法
}
}
HashMap不允许重复的key,它通过key的equals方法来判断。因此作为HashMap的key必须重写equals方法,同时也需要重写hashCode方法。
6.2HashMap的遍历
HashMap的遍历,HashMap没有实现iterable接口,因此不能使用foreach进行遍历。但是它的keySet方法可以返回其key的集合,可以实现遍历。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Day1909 {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String, String>();
map.put("a", "A");
map.put("b", "B");
map.put("c", "C");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println("key:" + key + ",value:" + map.get(key));
}
System.out.println();
Set<Map.Entry<String,String>> entrys = map.entrySet();
for (Map.Entry<String,String> entry : entrys) {
System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue());
}
}
}
7. LinkedHashMap
LinkedHashMap是HashMap的子类,并且其操作和HashMap相同。
HashMap是无序的,不保留元素的插入顺序,LinkedHashMap保留元素的插入顺序。当对LinkedHashMap进行迭代时,它将按照元素添加的顺序返回。
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
public class Day1910 {
public static void main(String[] args) {
// Map<String, String> map = new HashMap<String, String>();
Map<String,String> map = new LinkedHashMap<String, String>();
map.put("a", "A");
map.put("c", "C");
map.put("b", "B");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println("key:" + key + ",value:" + map.get(key));
}
}
}
8. TreeMap
TreeMap实现了Map接口和SortedMap接口,其操作和HashMap相同。
TreeMap是有序的。TreeMap内部将按照元素的key的大小顺序排序。
TreeMap和TreeSet一样,要求key实现了Comparable接口,或者在构造TreeMap时提供一个Comparator的实例。
8.1 key实现Comparable接口
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
public class Day1911 {
public static void main(String[] args) {
Map<Integer, String> map = new LinkedHashMap<Integer, String>();
map.put(3, "张三");
map.put(2, "李四");
map.put(1, "王五");
Set<Integer> keys = map.keySet();
for (Integer key : keys) {
System.out.println("key:" + key + ",value:" + map.get(key));
}
}
}
8.2 提供一个Comparator的实例
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class Day1911 {
public static void main(String[] args) {
Map<Integer, String> map = new TreeMap<Integer, String>(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2.intValue() - o1.intValue();//从大到小排序
}
});
map.put(1, "王五");
map.put(3, "张三");
map.put(2, "李四");
Set<Integer> keys = map.keySet();
for (Integer key : keys) {
System.out.println("key:" + key + ",value:" + map.get(key));
}
}
}
9. Queue接口
Queue为队列,队列是一种先进先出(FIFO)的数据结构。
Queue的实现类LinkedList
import java.util.LinkedList;
import java.util.Queue;
public class Day1912 {
public static void main(String[] args) {
Queue<String> q = new LinkedList<String>();
//将元素插入队列
q.offer("a");
q.offer("b");
q.offer("c");
System.out.println(q);//[a, b, c]
//获取但不移除队列的头
System.out.println(q.peek());//a
System.out.println(q);//[a, b, c]
//获取并移除队列的头
System.out.println(q.poll());//a
System.out.println(q);//[b, c]
System.out.println(q.poll());//b
System.out.println(q.poll());//c
System.out.println(q);//[]
}
}
10. Vector
Vector实现了List接口,内部由数组实现。但是与ArrayList不同的是,Vector是线程安全的,可以在多线程环境中使用。但同时也要注意其效率低于ArrayList。
import java.util.List;
import java.util.Vector;
public class Day1913 {
public static void main(String[] args) {
List<String> vector = new Vector<String>();
vector.add("a");
vector.add("b");
vector.add("c");
System.out.println(vector);//[a, b, c]
}
}
11. HashTable
HashTable实现了Map接口,其操作和HashMap类似。但是HashTable是线程安全的。但是要注意其效率低于HashMap。
import java.util.Hashtable;
public class Day1914 {
public static void main(String[] args) {
Hashtable<Integer, String> map = new Hashtable<Integer, String>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
System.out.println(map);//{3=c, 2=b, 1=a}
}
}
12. HashMap原理
12.1 数组,链表,散列表的区别
链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要査看某个指定的元素, 却又忘记了它的位置, 就需要访问所有元素, 直到找到为止。如果集合中包含的元素很多,将会消耗很多时间。如果不在意元素的顺序, 可以使用能够快速査找元素的数据结构:散列表。
12.2 散列表原理
散列表是存放entry的集合,每个entry包含一个key和一个value。散列表为每一个key计算一个整数, 称为散列码(hashcode)。 散列码是由key的数据域产生的一个整数。 具有不同数据域的key将产生不同的散列码。
在Java 中, 散列表用链表数组实现。数组中的每个元素被称为桶( bucket) 。
例如, 如果某个entry的key的散列码为15745, 并且有128 个桶, entry应该保存在第1 号桶中( 15745除以128 余1)。或许会很幸运, 在这个桶中没有其他元素, 此时将元素直接插人到桶中就可以了。
当然,有时候会遇到桶被占满的情况,这也是不可避免的,这种情况称为散列冲突,这时需要用新entry的key与桶中所有的entry的key进行比较,查看这个对象是否已经存在。如果存在,那么新的value将替换旧的value。如果不存在,那么就将新的entry插入到链表的末尾。
如果知道entry的key,需要在散列表中查询其value,那么首先需要计算其key值的散列码,然后对桶的总数取余,得到对应桶的索引位置。如果该索引处只有一个元素,那么就调用key的hashCode方法和equals方法进行对比。如果对比通过就返回其value,否则认为未找到。如果该索引处不止一个元素,而是一个链表,那么依次对此链表所有元 素的key调用hashCode方法和equals方法进行比较。
12.3 hashCode和equals方法
通过散列表的原理,我们不难发现,要想保证散列表正常使用。key必须保证其实现了正确的hashCode和equals方法。Object类自带HashCode和equals方法,因此其所有的子类都具备这两个方法。但是这个两个方法的实现在通常情况下并不合理。HashCode方法应该能使key在散列表上均匀分布,并且有效避免散列冲突。equals方法也不能只比较对象的内存地址,应该根据实际的情况来实现。幸运的是,如果我们使用String来作为key的类型,那么就不用关心这个两个方法的实现,因为String类重写了Object类的这两个方法。
12.4 再散列
如果在创建的时候不指定散列表的桶数,那么它将使用默认值是16(2的4次幂)。如果散列表太满,就需要进行再散列。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子是0.75(默认值),就是说如果表中超过了75%的位置已填入元素,这个表就会用双倍的桶数进行再散列。
如果预先知道需要放入多少个元素,最好再创建散列表时就指定其初始容量(initial capacity),已防止其自动的再散列。但是散列表的容量总是2的n次幂,因为这样能使散列分布的更加均匀并且提高散列表的查询效率。
14. 练习
key调用hashCode方法和equals方法进行比较。
12.3 hashCode和equals方法
通过散列表的原理,我们不难发现,要想保证散列表正常使用。key必须保证其实现了正确的hashCode和equals方法。Object类自带HashCode和equals方法,因此其所有的子类都具备这两个方法。但是这个两个方法的实现在通常情况下并不合理。HashCode方法应该能使key在散列表上均匀分布,并且有效避免散列冲突。equals方法也不能只比较对象的内存地址,应该根据实际的情况来实现。幸运的是,如果我们使用String来作为key的类型,那么就不用关心这个两个方法的实现,因为String类重写了Object类的这两个方法。
12.4 再散列
如果在创建的时候不指定散列表的桶数,那么它将使用默认值是16(2的4次幂)。如果散列表太满,就需要进行再散列。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子是0.75(默认值),就是说如果表中超过了75%的位置已填入元素,这个表就会用双倍的桶数进行再散列。
如果预先知道需要放入多少个元素,最好再创建散列表时就指定其初始容量(initial capacity),已防止其自动的再散列。但是散列表的容量总是2的n次幂,因为这样能使散列分布的更加均匀并且提高散列表的查询效率。
14. 练习
1,编写程序,在main方法中接受5个整数,创建TreeSet类型的集合,将5个整数添加到集合中,使用foreach遍历该集合。提供一个比较器,然后对于集合中的整数进行从大到小的顺序进行排序。然后使用Iterator进行迭代。