JavaSE 学习笔记05丨泛型、集合
Chapter. 10 泛型
10.1 泛型程序设计
泛型,指可以在类或方法中预支地使用未知的类型。泛型程序设计(Generic programming),意味着编写的代码可被很多不同类型的对象所重用。使用泛型机制编写的程序代码,要比那些杂乱使用Object
变量然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,如集合类ArrayList
。
10.1.1 引入
在Java SE5.0之前,Java泛型程序设计是用继承实现的。例如ArrayList
类只维护一个Object
引用的数组:
public class JustTest{ //before Java SE 5.0
public Object get(int i){...}
public void add(Object o){...}
...
private Object[] elementData;
}
如此实现有两个问题,当获取一个值时必须将返回的Object
类进行强制类型转换。
ArrayList files = new ArrayList();
...
String filename = (String) names.get(0);
此外,下面这里不存在错误检查,可向数组列表中添加任何类的对象
files.add(new File("..."));
10.1.2 使用泛型的好处
泛型为上面所产生的问题提供了一个更好的解决方案——类型参数(type parameters)。其带来的好处:
-
代码具有更好的可读性。
ArrayList<String> files = new ArrayList<String>(); //一看便知道该数组列表中包含的是String对象
-
编译器能够很好地利用信息,无需进行强制类型转换。
String filename = files.get(0); //当调用get()无需强转,编译器便知道返回值类型为String,而不是Object String str = it.next(); //使用迭代器无需强转
一般在创建对象时,将未知的类型确定具体类型。若没有指定泛型,则默认类型为
Object
类。 -
编译器可以进行检查,避免插入错误类型的对象。将运行时的
ClassCastException
转移到了编译时异常。files.add("abcd"); //Correct //files.add(5); 当集合明确类型后存放类型不一致就会编译报错!
泛型,是数据类型的一部分。我们将类名与泛型合并一起看做数据类型。
10.2 泛型的定义与使用
TIPS:类型变量 使用大写形式且比较短。在Java库中,使用变量
E
表示集合的元素类型,K
和V
分别表示表的关键字与值的类型,T
表示任意类型。
10.2.1 含有泛型的类
定义格式为:修饰符 class 类名<代表泛型的变量>{ }
class ArrayList<E>{ //API中的ArrayList集合
public boolean add(E e){ ... }
public E get(int index){ ... }
...
}
public class MyGenericClass<MVP>{ //自定义的泛型类
private MVP mvp;
public void setMVP(MVP mvp){
this.mvp = mvp;
}
public MVP getMVP(){
return mvp;
}
}
public class Pair<T, U> { ... } //泛型类可以有多个类型变量,此处的Pair类第一个域和第二个域使用不同的类型
在创建对象时,就需要将泛型确定下来。即用具体的类型替换类型变量就可以实例化泛型类型,例如:
ArrayList<Integer> list = new ArrayList<Integer>(); //在对象list中,所有的E类型都替换为String类型
MyGenericClass<String> a = new MyGenericClass<String>();
String mvp = a.getMVP();
MyGenericClass<Integer> b = new MyGenericClass<Integer>();
Integer mvp2 = b.getMVP();
10.2.2 含有泛型的方法
泛型方法可以定义在普通类中,也可以定义在泛型类中。
定义格式为:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
public class MyGenericMethod{
public <T> void show(T mvp){
System.out.println(mvp.getClass());
}
public <T> T show2(T mvp){
return mvp;
}
}
调用泛型方法时,大多情况下方法调用可以省略<…>具体的类型参数,编译器有足够的信息能够推断出所调用的方法。
String middle = Array.show2(names);
10.2.3 含有泛型的接口
定义格式为:修饰符 interface 接口名<代表泛型的变量> { }
public interface MyGenericInterface<E>{
public abstract void add(E e);
public abstract E getE();
}
使用方式:
-
定义实现类时即确定泛型的类型。
public class MyImp1 implements MyGenericInterface<String>{ @Override public void add(String e){ //方法参数从类型参数改为String //... } public String getE(){ //返回类型从类型参数改为String //... } }
-
始终不确定泛型的类型,直至创建对象时才确定泛型的类型。
public class MyImp2 implements MyGenericInterface<E>{ @Override public void add(E e){ //... } public E getE(){ //... } }
确定泛型:
MyImp2<String> my = new MyImp2<String>(); my.add("2333");
10.3 泛型通配符
10.3.1 通配符的基本使用
当使用泛型类或者接口时,传递的数据中泛型类型不确定,可以通过<?>
表示,其中?
表示未知通配符。但一旦使用泛型的通配符后,只能使用Object
类中的共性方法,集合中元素自身方法无法使用;只能接受数据,而不能往该集合中存储数据。
public class JustTest {
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){
//....
}
}
TIPS:泛型不存在继承关系,如:
Collection<Object> list = new ArrayList<String>();
这种是错误写法!
10.3.2 通配符限定
Java的泛型可以指定一个泛型的上限或下限。
-
泛型的上限
格式:
类型名称 <? extends 类> 对象名称
意义:只能接收指定类型或其子类。
public static void getElements1(Collection<? extends Number> coll){ ... } //此时的泛型 ? ,必须是Number类型或者Number类型的子类
-
泛型的下限
格式:
类型名称 <? super 类> 对象名称
意义:只能接收指定类型或其父类。
10.4 泛型的约束与局限性
-
不能用基本类型实例化类型参数。
比如说,没有
Pair<double>
,而只有Pair<Double>
。其原因在于类型擦除,Pair
类含有Object
类型的域,而Object
不能存储double
值 -
运行时类型查询只适用于原始类型。
if(a instanceof Pair<String>){ /*...*/ } //true,实际上仅仅测试a是否为任意类型的Pair if(a instanceof Pair<T>) { /*...*/ } //true Pair<String> a = ...; Pair<Employee> b = ...; if(a.getClass() == b.getClass()){ /*...*/ } //比较结果为true,因为两次调用getClass都将返回Pair.class Pair<String> p = (Pair<String>) a; //WARNING---can only test that a is a Pair
-
不能抛出也不能捕获泛型类实例。
事实上,泛型类拓展
Throwable
都不合法。public class MyProblem<T> extends Exception{ /*...*/ } //不会通过编译,Error!
不能再
catch
子句中使用类型变量,如下不能通过编译。public static <T extends Throwable> void doWork(Class<T> t){ try { /*do work*/ } catch (T e){ //ERROR--can't catch type variable!!! //... } }
但在异常声明中可以使用类型变量,如下是合法的
public static <T extends Throwable> void doWork(Class<T> t) throws T{ try { /*do work*/ } catch (T e){ //ERROR--can't catch type variable!!! //... } }
-
参数化类型的数组不合法。
Pair<String> table = new Pair<String>[10]; //ERROR //若需要收集参数化类型的对象,最好使用ArrayList ArrayList<Pair<String>> table = ....
-
不能实例化类型变量。
不能使用像
new T(...)
、new T[...]
或者T.class
这样的表达式中的类型变量。 -
泛型类的静态上下文中类型变量无效。
-
注意擦除后的冲突。
Chapter 11. 集合
11.1 集合概述
从Java SE 5.0 开始,集合类是带有类型参数的泛型类。Java集合类库将接口与实现分离。其集合按照其存储结构可分为两大类:单列集合java.util.Collection
、双列集合Java.util.Map
。
实现 Collection
接口的类
集合类型 | 描述 |
---|---|
\(ArrayList\) | 可动态增长和缩减的索引序列 |
\(LinkedList\) | 可在任何位置进行高效插入和删除的有序序列 |
\(ArrayDeque\) | 循环数组实现的双端队列 |
\(HashSet\) | 无重复元素且无序 的散列集 |
\(TreeSet\) | 有序 集 |
\(LinkedHashSet\) | 可记住元素插入次序的 集(链表+散列表实现) |
\(PriorityQueue\) | 高效删除最小元素的机会 |
\(BitSet\)(遗留) | 存放位序列的位集(功能同C++的位集模板) |
实现 Map
接口的类
集合类型 | 描述 |
---|---|
\(HashMap\) | 存储键-值关联的数据结构 |
\(TreeMap\) | 键值有序排列的映射表 |
\(LinkedHashMap\) | 可记住键-值添加次序的映射表 |
11.2 Collection 接口 与 Collections 工具类
11.2.1 Collection 继承结构
Collection
接口,为单列集合类的根接口,用于存储一系列符合某种规则的元素。
它有两个重要子接口:java.util.List
和java.util.Set
。
-
List
特点为元素是有序的(指存储和取出元素的顺序是一致的)、且允许存储重复元素、有索引(故可用普通for
循环进行遍历),它的主要实现类为java.util.ArrayList
和java.util.LinkedList
; -
Set
特点为元素不一定有序、不允许存储重复元素、没有索引(故不能用普通的for
循环进行遍历),它的主要实现类为java.util.HashSet
和java.util.TreeSet
。
11.2.2 Collection 常用方法
Collection
中定义了单列集合(List
和Set
)通用的方法:
-
public boolean add(E element)
:将对象element
添加至当前集合中。Collection<String> coll = new ArrayList<String>(); coll.add("Luffy");
-
public void clear()
:清空集合中所有的元素。 -
public boolean remove(E element)
:在当前集合中删除对象element
。 -
public boolean contains(E element)
:判断当前集合中是否包含对象element
。 -
public boolean isEmpty()
:判断当前集合是否为空。 -
public int size()
:返回集合中元素的个数。 -
public Object[] toArray()
:将集合中元素,存储至数组中。Object[] arr = coll.toArray(); for(int i = 0; i < arr.length(); i++){ System.out.println(arr[i] + " "); }
11.2.3 Collections 工具类
java.utils.Collections
是集合工具类,用于对集合进行相关操作。常用方法有:
-
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合
c
中添加多个相同类型的元素elements
。ArrayList<Integer> mylist = new ArrayList<Integer>(); Collections.addAll(mylist, 233, 666, 1, 2); /* 上语句等价于下面写法 mylist.add(233); mylist.add(666); mylist.add(1); mylist.add(2); */
-
public static void shuffle(List<?> list)
: 使用默认随机源对列表进行置换,所有置换发生的可能性都是大致相等的。(简而言之,打乱集合顺序)List<Integer> mylist = new ArrayList<Integer>(); for(int i = 1; i <= 10; i++) mylist.add(i); for(int i = 1; i <= 6; i++){ System.out.println("第" + i + "次洗牌:"); Collections.shuffle(mylist); System.out.println(mylist); }
-
public static<T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static<T> void sort(List<T> list, Comparator<? super T>)
:将集合中元素按照指定规则排序。
11.2.4 Comparator 比较器
Java中提供两种对象间比较大小的实现方式:
-
采用
java.lang.Comparable
接口实现——强行对实现它的每个类的对象进行整体排序。(类的自然排序)有些标准的Java平台类,实现了
Comparable
接口,而该接口定义了compareTo
方法。public interface Comparable<T>{ int compareTo(T others); }
而对于自定义的对象,必须通过实现
Comparable
接口自定义排列顺序,因为在Object
类中没有提供任何compareTo
接口的默认实现。如下展示了如何用“部件编号”为Item
对象进行排序。实现Comparable
接口的对象列表(和数组)可以通过Collections.sort
或Arrays.sort
进行排序。class Item implements Comparable<Item>{ public int compareTo(Item other){ return partNum - other.partNum; //若第一项位于第二项前面,则返回负值;编号相同返回0;否则返回正值。 } }
显然对于上面的
Comparable
接口定义排序有其局限性,对于一个给定的类,只能够实现这个接口一次(也就说只能在类中实现compareTo()
一次)。如上例子,如果我希望在一个集合中按“部件编号”排序,在另一集合按描述信息排序,同时这个类创建者又没有实现Comparable
接口,此时就需要下面的Comparator
接口。 -
java.util.Comparator
接口实现——强行对某个对象进行整体排序。Comparator
接口声明了一个带两个显式参数的compare
方法。public interface Comparator<T>{ int compare(T a, T b); }
该接口代表一个比较器。这个比较器没有任何数据,只是比较方法的持有器,可以将
Comparator
传递给sort
方法(如Collections.sort
或Arrays.sort
),或者传递给某些数据结构(如TreeSet
)。SortedSet<Item> sortByDescription = new TreeSet<Item>(new Comparator<Item>(){ @Override public int compare(Item a, Item b){ String descrA = a.getDescription(); String descrB = a.getDescription(); return descrA.compareTo(descrB); } }); //将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法 Collections.sort(list, new Comparator<Student>(){ @Override public int compare(Student a, Student b){ return a.getAge() - b.getAge(); } });
11.3 Iterator 接口与迭代器
11.3.1 概述
JDK专门提供一个接口java.util.Iterator
,用于遍历集合中的元素。Iterator
接口属于Java集合,但与Collection
、Map
这两个专门用于存储元素的接口不同,Iterator
主要用于迭代访问(即遍历)元素,因而,Iterator
对象被称为迭代器。
使用迭代器时,元素被访问的顺序取决于集合类型。若对ArrayList
进行迭代,迭代器会从索引0开始每迭代一次便+1;若访问HashSet
中元素,每个元素将按照某种随机的次序出现(尽管该迭代过程一定能遍历到集合所有元素,但却无法预知元素被访问的次序)。
11.3.2 相关方法
Collection
接口拓展了Iterable
接口,获取迭代器的方法为:
public Iterator<E> iterator()
:返回一个实现了Iterator
接口的对象,可以使用该迭代器对象依次访问集合中的元素。
而Iterator
接口包含以下常用方法:
-
public E next()
:调用next
时,迭代器会越过下一个元素,并返回刚刚越过的那个元素的的引用。当到达集合末尾时next
方法会抛出NoSuchElementException
。 -
public boolean hasNext()
:如果迭代器对象还有多个供访问的元素,返回true
。反之返回false
。public class JustTest { public static void main(String[] args) { Collection<String> arr = new ArrayList<String>(); arr.add("Luffy"); arr.add("Nami"); arr.add("Zoro"); Iterator<String> it = arr.iterator(); while(it.hasNext()){ String tmp = it.next(); System.out.println(tmp); } } }
-
public void remove()
:删除上次调用next
方法时返回的元素。(也就说,想要删除指定位置上的元素,仍然需要越过该元素)。当调用remove
之前没有调用next
是不合法的,抛出IllegalStateException
异常Iterator<String> it = c.iterator(); it.next(); //越过第一个元素 it.remove(); //将刚越过的元素删去
应该将Java迭代器认为是位于两个元素之间的。C++标准模板库中迭代器是根据数组索引建模的,不需查找元素即可将迭代器向前移动一个位置。而Java不同,查找操作与位置变更紧密相连,查找一个元素的唯一方法是调用
next
,而在执行查找操作同时迭代器位置随之向前移动。
11.3.3 for each 循环
编译器将for each
循环翻译为带有迭代器的循环,故使用for each
循环遍历时,不能对集合元素进行增删操作。
for(元素的数据类型 变量名 : Collection集合或者数组){
//do something
}
public class JustTest {
public static void main(String[] args) {
Collection<String> arr = new ArrayList<String>();
arr.add("Luffy");
arr.add("Nami");
arr.add("Zoro");
for(String tmp: arr){
System.out.println(tmp);
}
}
}
11.4 List 接口与其子类
List
作为 Collection
集合的子接口之一,它用于描述一个有序集合,并且集合中每个元素的位置十分重要。它有两种访问元素的协议:使用迭代器,或用 get
和 set
方法随机地访问每个元素(不适用链表,适用数组)。
11.4.1 数组列表(ArrayList )
java.util.ArrayList
封装了一个动态再分配的对象数组。元素增删慢,查找快。
ArrayList
方法不是同步的,故建议在不需要同步时使用ArrayList
,比使用Vector
更高效。
11.4.2 链表(LinkedList)
链表将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用。而在Java中,所有链表实际上都是双向链表(doubly linked)的。而java.util.LinkedList
集合数据存储的结构即为双向链表,元素增删块,查找慢。
以下为LinkedList
常用方法的举例使用:
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
public class LinkedListTest {
public static void main(String[] args) {
List<String> staff = new LinkedList<String>();
//调用LinkedList的add方法:将对象添加至链表尾部
staff.add("Luffy");
staff.add("Zoro");
staff.add("Sanji");
//遍历LinkedList
ListIterator<String> iter_1 = staff.listIterator();
while(iter_1.hasNext()){
String tmp = iter_1.next();
System.out.println(tmp);
}
ListIterator<String> iter_2 = staff.listIterator();
//越过第一个元素,并返回该元素至oldName
String oldName = iter_2.next();
//调用ListIterator的Set方法,用一新元素取代调用next或previous方法返回的上一个元素
iter_2.set("Monkey.D.Luffy");
//调用子接口ListIterator的previous方法,返回越过的对象
if(iter_2.hasPrevious()) iter_2.previous();
//调用子接口ListIterator的add方法(注意区分上面的Collection的add方法),将对象添加至迭代器位置之前
iter_2.add("Nami");
}
}
由于迭代器是描述集合中位置的,故这种依赖位置的
add
方法将由迭代器负责,只有对自然有序的集合使用迭代器添加元素才有实际意义。
11.5 Set 接口与其子类
Set
作为 Collection
集合的子接口之一,它由散列表所实现,由于散列将元素分散在表的各个位置上,故其Set
接口中元素是无序的(TreeSet
除外),并以某种规则保证存入元素是不重复的。
11.5.1 基于散列表的集(HashSet)
如果想要查看某个指定的元素,但不在意元素的顺序,散列表(hash Table)正能够满足于此。在散列表中,具有不同数据域的对象产生了不同的散列码(hash code)。
HashSet
类,正实现了基于散列表的集。它根据对象的散列码来确定元素在集合中的存储位置,它保证元素唯一性的方式依赖于hashCode
方法与equals
方法。由此,在用HashSet
存放自定义类型元素时,需要覆写对象中的hashCode
方法(没覆写的话每个新对象的hashCode
值均不同)和equals
方法(没覆写的话就是比较地址值),建立自己的比较方式方能保证HashSet
集合中的对象唯一。
hashCode
值是由操作系统产生的逻辑地址值,而地址值才是真实的物理地址。
/* Student.java文件中 */
public class Student{
private String name;
private int age;
public Student(){
}
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 boolean equals(Object o){ //覆写equals方法
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, name);
}
@Override
public int hashCode(){ //覆写hashCode方法
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
/* 测试类 HashSetTest.java */
public class HashSetTest{
public static void main(String[] args){
HashSet<Student> MySchool = new HashSet<Student>();
MySchool.add(new Student("Luffy", 18));
MySchool.add(new Student("Nami", 23));
MySchool.add(new Student("Luffy", 18));
for(Student peo : MySchool){
System.out.println(peo);
}
}
}
/* 运行结果 */
Student{name='Nami', age=23}
Student{name='Luffy', age=18}
11.5.2 树集(TreeSet)
TreeSet
类与散列集十分类似,但它比`散列集有所改进。树集是一个有序集合(sorted collection),它可以以任意顺序将元素插入到集合中,当在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现,(其中,排序是利用红黑树去完成的,每次将一个元素添加至树中时,都被放置在正确的排序位置上)故迭代器总以排好序的顺序去访问每个元素。
默认情况下,树集假定插入元素实现了
Comparable
接口(若元素为自定义对象,则需实现Comparable
接口自定义排序)。对于基本类型的包装类和String
默认为升序。
总而言之,将一个元素添加到TreeSet
中要比添加到HashSet
中会慢一些,不过TreeSet
可自动对元素进行排序。
public class TreeSetTest {
public static void main(String[] args) {
Set myset = new TreeSet();
myset.add(233);
myset.add(666);
myset.add(1);
myset.add(42);
myset.add(233);
for(Iterator iter = myset.iterator(); iter.hasNext();){
System.out.println(iter.next());
}
}
}
11.6 Map 接口
集(set)是一个集合,可以快速查找现有元素,但要查看一个元素,需要查找元素的精确副本,但这并不是非常通用的查找方式。
通常,我们知道某些键的信息,并想要查找与之对应的元素,映射表(map)数据结构就是为此实现的。映射表用来存放键/值对,若提供了键,就能查到值。
Java类库为映射表提供两个通用实现:
HashMap
:散列映射表,对键进行散列。TreeMap
:树映射表用键的整体顺序对元素进行排序。
尽管
HashTable
已被取代,但其子类Properties
仍未退出历史舞台,在I/O流章节中会专门讲述。
键必须是唯一的,不能对同一个键存放两个值,每个键只能对应一个值。
注意:
- 使用
HashMap
存放自定义对象时,要保证对象唯一,必须覆写对象的hashCode
和equals
方法。 - 若希望保证map存放的
key
和取出的顺序一致,可使用java.util.LinkedHashMap
集合来存放。
11.6.3 Map常用方法
public V put(K key, V value)
:将指定的键key
与值value
添加至Map
集合中。若指定的键key
在该集合中没有,则将指定的键值添加到集合中,并返回null
;若指定的键key
在集合中存在,则返回值为集合中键对应的值(指替换前的值),并将指定键所对应的值替换为指定的新值value
。public V remove(Object key)
:将 指定的键key
所对应的 键值对元素 在Map
集合中删除,同时返回被删除元素的 值 。public V get(Object key)
:在Map
集合中获取 指定的键key
所对应的值。public Set<K> keySet()
:获取Map
集合中所有的 键 ,存储到Set
集合中。注意,keySet
既不是HashSet
也不是TreeSet
。public Set<Map.Entry<K, V>> entrySet()
:获取Map
集合中所有的键值对对象的Set
集合。
11.6.2 Map的遍历方式
一、键找值方式
获取Map
中所有键,由于键是唯一的,利用keySet
方法得到一个存储所有键的Set
集合,再遍历该Set
集合的每一个键,根据键调用get()
方法来获取键所对应的值。
HashMap<Integer, String> mymap = new HashMap<Integer, String>();
mymap.put(20190301, "Luffy");
mymap.put(20190304, "Nami");
mymap.put(20190302, "Zoro");
Set<Integer> mykeys = mymap.keySet();
for(int curkey: mykeys){
String curname = mymap.get(curkey);
System.out.println(curkey + "所对应的学生姓名为:" + curname);
}
二、键值对方式
Map
中的key
与value
是一一对应的关系,而每一个键值对又称为Map
中的一个项(Entry
),Entry
将键值对的对应关系封装为对象,称为键值对对象,或称为 条目
在遍历Map
集合时,就可从每一键值对Entry
对象中获取对应的键与值。对于Entry
,也提供了相应的方法:
public K getKey()
:获取Entry
对象中的键。public V getValue()
:获取Entry
对象中的值。public Set<Map.Entry<K, V>> entrySet()
:获取Map
集合中所有的键值对对象的集合。
HashMap<Integer, String> mymap = new HashMap<Integer, String>();
mymap.put(20190301, "Luffy");
mymap.put(20190304, "Nami");
mymap.put(20190302, "Zoro");
Set<Map.Entry<Integer, String>> mySet = mymap.entrySet();
for(Map.Entry<Integer, String> a : mySet){
System.out.println(a.getValue() + "的学号为:" + a.getKey());
}