JAVA程序设计基础-第6版陈国君2006-学习笔记3
[TOC]
JAVA程序设计基础-第6版陈国君2006-学习笔记3
第十二章 泛型与容器类
泛型技术可以通过一种类型或方法操纵各种不同类型的对象,同时又提供了编译时的类型安全保证。
泛型
泛型其实质就是将数据的类型参数化。
编写的代码可以被很多类型不同的对象所重用,从而减少数据类型转换的潜在错误。
泛型的概念
用泛型的主要优点是能够在编译时而不是在运行时检测出错误。
泛型实际上是在定义类、接口或方法时通过为其增加”类型参数”来实现的。
类型参数-type parameters
:泛型所操作的数据类型被指定为一个参数。
- 泛型类的定义
1 | [修饰符] class 类名 <T> |
- 泛型接口的定义
1 | [public] interface 接口名 <T> |
- 泛型方法的定义
1 | [public] [static] <T> 返回值类型方法名 (T参数) |
用泛型类创建的对象就是在类体内的每个类型参数T处分别用这个具体的实际类型替代。
泛型的实际参数必须是类类型。
泛型实例化
:利用泛型类创建的对象称为泛型对象。
泛型也称为参数多态
泛型类及应用
在泛型实例化时,可以根据不同的需求给出类型参数T的具体类型。
调用泛型类的方法传递或返回数据类型时可以不用进行类型转换,而是直接用T作为类型来代替参数类型或返值的类型。
在实例化泛型类的过程中,实际类型必须是引用类型,即必须是类类型,不能用如int、double或char等这样的基本类型来替换类型参数T。
1 | public class App12_1 <T> { |
T可代表任意一种数据类型,并可用该类型来声明类成员变量、成员方法的参数或返回值等。
自动包装
:应该使用包装类对象的地方却使用基本数据类型的数据时,编译器将自动把该数据包装为该基本类型对应的包装类的对象。
当一个泛型有多个类型参数时,每个类型参数在该泛型中都应该是唯一的。如不能定义形如Map<K,K>形式的泛型,但可以定义Map<K,V>形式的泛型。
泛型方法
一个方法是否是泛型方法与其所在的类是否是泛型类没有关系。
要定义泛型方法,只需将泛型的类型参数 < T > 置于方法返回值类型前即可。在Java中任何方法(包括静态方法和构造方法)都可声明为泛型方法。泛型方法除了定义不同,调用时与普通方法一样。
1 | public class App12_2 { |
可将实际类型放在尖括号内作为方法名的前缀。
泛型方法的返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的。
推荐使用返回值类型和参数类型一致的泛型方法。
一个static方法,无法访问泛型类的类型参数,所以如果static方法需要使用泛型能力,必须使其成为泛型方法。
当使用泛型类时,必须在创建泛型对象的时候指定类型参数的实际值,而调用泛型方法时,通常不必指明参数的类型。
Java编译器的类型参数推断
:它会根据调用方法时实参的类型,推断得出被调用方法类型参数的具体类型,并据此检查方法调用中类型的正确性。
泛型方法不需要把实际的类型传递给泛型方法。
泛型类必须把实际的类型参数传递给泛型类。
限制泛型的可用类型
定义泛型类时,默认可以使用任何类型来实例化一个泛型类对象,但Java语言也可以在用泛型类创建对象时对数据类型做出限制。
1 | class ClassName <T extends anyClass> |
1 | class GneeralType<T extends Number> { |
利用泛型进行实例化时,若泛型的实际参数的类之间有父子关系时,参数化后得到的泛型类之间并不会具有同样的父子类关系,即子类泛型”并不是一种”父类泛型。
泛型的类型通配符和泛型数组的应用
通配符的主要作用:
- 用于创建可重新赋值但不可修改其内容的泛型对象;
- 用在方法的参数中,限制传入不想要的类型实参。
一个程序中使用同一个泛型对象名去引用不同的泛型对象时,就需要使用通配符”?”创建泛型类对象。
1 | 泛型类名 <? extends T> o = null; |
“?extends T”表示是T或T的未知子类型或是实现接口T的类。
如果只使用了”?”通配符,则默认是”? extends Object”,所以”?”也被称为非受限通配。
用通配符”?”创建的对象只能获取或删除其中的信息,但不能对其加入新的信息。
直接用通配符 < ? > 创建泛型对象,有两个特点:
- 具有通用性,即该泛型类的其他对象可以赋值给用通配符”?”创建的泛型对象,因为”?”等价于”? extends Object”,反之不可。
- 用通配符”?”创建的泛型对象,只能获取或删除其中的信息,但不可为其添加新的信息。
上限通配:”? extends T”,T或T的一个未知子类。
下限通配:”? super T”,T或T的一个未知父类。
由于JVM只是在编译时对泛型进行安全检查,所以特别强调以下几点。
- 不能使用泛型的类型参数T创建对象。如
T obj = new T()
是错误的。 - 在泛型中可以用类型参数T声明一个数组,但不能使用类型参数T创建数组对象。例如
T[] a = new T[个数]
是错误的。 - 不能在静态环境中使用泛型类的类型参数T。
1 | public class Test<T> { |
- 异常类不能是泛型的,即泛型类不能继承
java.lang.Throwable
类 。
继承泛型类与实现泛型接口
被定义为泛型的类或接口可被继承与实现。
1 | public class ExtendClass <T1> { } |
如果在SubClass类继承ExtendClass类时保留父类的类型参数,需要在继承时指明,如果没有指明,直接使用extends ExtendClass语句进行继承声明,则SubClass类中的T1、T2和T3都会自动变为Object,所以在一般情况下都将父类的类型参数保留。
在定义泛型接口时,泛型接口也可被实现。
1 | interface Face <T1> { } |
容器类
容器类是Java以类库的形式供用户开发程序时可直接使用的各种数据结构。
数据结构就是以某种方式将数据组织在一起,并存储在计算机中。
Java容器框架
Java容器框架结构由两棵接口树构成:
第一棵树根节点为Collection接口,它定义了所有容器的基本操作,如添加、删除、遍历等。它的子接口Set、List等则提供了更加特殊的功能,其中Set的对象用于存储一组不重复的元素集合,而List的对象用于存储一个由元素构成的线性表。
第二棵树根节点为Map接口,它保持了”键”到”值”的映射,可以通过键来实现对值的快速访问。
容器接口Collection
容器接口Collection通常不能直接使用,但该接口提供了添加元素、删除元素、管理数据的方法。
容器框架全部采用泛型实现,所以我们以泛型的形式给出相应的方法,即带类型参数。
- Collection < E > 接口的常用方法
常用方法 | 功能说明 |
---|---|
int size() | 返回容器中元素的个数 |
boolean isEmpty() | 判断容器是否为空 |
boolean contains(Object obj) | 判断容器是否包含元素obj |
boolean add(E element) | 向容器中添加元素element,添加成功返回true;若容器中已包含element,且不允许有重复元素,则返回false |
int hashCode() | 返回容器的哈希码值 |
Object[] toArray() | 将容器转换为数组,返回的数组包含容器的所有元素 |
boolean remove(Object obj) | 从容器中删除元素obj,删除成功返回true;若容器不包含obj,则返回false |
void clear() | 删除容器中的所有元素 |
Iterator < E > iterator() | 返回容器的迭代器 |
boolean equals(Object o) | 比较此collection与指定对象o是否相等 |
void shuffle(List < ? > list) | 以随机方式重排list 中的元素,即洗牌 |
boolean containsAll(Collection < ? > c) | 判断当前容器是否包含容器c中的所有元素 |
boolean addAll(Collection < ? extends E > c) | 将容器c中的所有元素添加到当前容器中,集合并运算 |
boolean removeAll(Collectio n< ? > c) | 在当前容器中删除包含在容器c中的所有元素,集合差运算 |
boolean retainAll(Collection < ? > c) | 仅保留当前容器中也被容器c包含的元素,即删除当前容器中未被包含在容器c中的所有元素,集合交运算 |
列表接口List(可重复、可为空、有顺序)
列表接口List是Collection子接口,它是一种包含有序元素的线性表,其中的元素必须按顺序存放,且可重复,也可以是空值null。
元素之间的顺序关系可以由添加到列表的先后来决定,也可由元素值的大小来决定。
实现List接口的类主要有两个:链表类LinkedList
和数组列表类ArrayList
。它们都是线性表。
- List < E > 接口常用方法
常用方法 | 功能说明 |
---|---|
E get(int index) | 返回列表中指定位置的元素 |
E set(int index, E element) | 用元素element取代index位置的元素,返回被取代的元素 |
int indexOf(Object o) | 返回元素o首次出现的序号,若0不存在返回-1 |
int lastIndexOf(Object o) | 返回元素o最后出现的序号 |
void add(int index, E element) | 在index位置插人元素element |
boolean add(E element) | 在列表的最后添加元素element |
E remove(int index) | 在列表中删除index位置的元素 |
boolean addAll(Collection < ? extends E > c) | 在列表的最后添加容器c中的所有元素 |
boolea addAll(int index, Collection < ? extends E > c) ) | 在index位置按照容器c中元素的原有次序插入c中所有元素 |
ListIterator < E > listIterator() | 返回列表中元素的列表迭代器 |
ListIterator < E > listIterator(int index) | 返回从index位置开始的列表迭代器 |
ArrayList:通过下标随机访问元素,但除了在末尾处之外,不在其他位置插入或删除元素
LinkedList:需要在线性表的任意位置上进行插入或删除操作。
LinkedList
LinkedList链表类采用链表结构保存对象,使用循环双链表实现List。
这种结构向链表中任意位置插入、删除元素时不需要移动其他元素,链表的大小是可以动态增大或减小的,但不具有随机存取特性。
- LinkedList< E >类构造方法
构造方法 | 功能说明 |
---|---|
public LinkedList() | 创建空的链表 |
public LinkedList(Collection < ? extends E > c) | 创建包含容器c中所有元素的链表 |
- LinkedList< E >类常用方法
常用方法 | 功能说明 |
---|---|
public void addFirst(E e) | 将元素e插入到列表的开头 |
public void addLast(E e) | 将元素e添加到列表的末尾 |
public E getFirst() | 返回列表中的第一个元素 |
public E getLast() | 返回列表中的最后一个元素 |
public E removeFirst() | 删除并返回列表中的第一个元素 |
public E removeLast() | 删除并返回列表中的最后一个元素 |
ArrayList
ArrayList数组列表类使用一维数组实现List,该类实现的是可变数组,允许所有元素,包括null。具有随机存取特性,插入、删除元素时需要移动其他元素,当元素很多时插入、删除操作的速度较慢。
在向ArrayList中添加元素时,其容量会自动增大,但不能自动缩小,但可以使用trimToSize()方法将数组的容量减小到数组列表的大小。
- ArrayList< E >类构造方法
构造方法 | 功能说明 |
---|---|
public ArrayList() | 创建初始容量为10的空数组列表 |
public ArrayList(int initialCapacity) | 创建初始容量为initialCapacity的空数组列表 |
public ArrayList(Collection < ? extends E > c) | 创建包含容器c所有元素的数组列表,元素次序与c相同 |
- ArrayList< E >类常用方法
常用方法 | 功能说明 |
---|---|
public void trimToSize() | 将ArrayList对象的容量缩小到该列表的当前大小 |
public void forEach(Consumer < ? super E > action) | 将action对象执行遍历操作 |
容器遍历
对于容器中元素进行访问时,经常需要按照某种次序对容器中的每个元素访问且仅访问一次,这就是遍历,也称为迭代。遍历是指从容器中获得当前元素的后续元素。
foreach循环语句
元素类型、循环变量的名字(用于存储连续的元素)、从中检索元素的数组。
1 | for (type element : array) { |
toArray()转换数组
利用Collection接口中定义的toArray()方法将容器对象转换为数组,然后再利用循环语句对数组中的每个元素进行访问。
1 | Object[] e = c.toArray(); |
size()/get()方法
利用size()和get()方法进行遍历。即先获取容器内元素的总个数,然后依次取出每个位置上的元素并访问。
1 | for (int i=0; i<c.size(); i++) { |
迭代器
迭代功能由可迭代接口Iterable和迭代器接口Iterator、ListIterator实现的,迭代器是一种允许对容器中元素进行遍历并有选择地删除元素的对象。
由于Collection接口声明继承Iterable接口,因此每个实现Collection接口的容器对象都可调用iterator()方法返回一个迭代器。
- Iterator <E>接口的常用方法
常用方法 | 功能说明 |
---|---|
public abstract boolean hasNext() | 判断是否还有后续元素,若有则返回true |
public abstract E next() | 返回后续元素 |
public abstract void remove() | 删除迭代器当前指向的(即最后被迭代的)元素,即删除由最近一次next()或previous()方法调用返回的元素 |
1 | Iterator it = c.iterator(); |
对于容器中元素的遍历次序,接口Iterator支持对List对象从前向后的遍历,但其子接口ListIterator支持对List对象的双向遍历。
- ListIterator <E>接口的常用方法
常用方法 | 功能说明 |
---|---|
public abstract boolean hasPrevious() | 判断是否有前驱元素 |
public abstract E prevoius() | 返回前驱元素 |
public abstract void add(E e) | 将指定的元素插人列表中。若next()方法的返回值非空,该元素被插人到next()方法返回的元素之前;若previous()方法的返回值非空,该元素被插人到previous()方法返回的元素之后;若线性表没有元素,则直接将该元素加入其中 |
public abstract void set(E e) | 用元素e替换列表的当前元素 |
public abstract int nextIndex() | 返回基于next()调用的元素序号 |
public abstract int prevoiusIndex() | 返回基于previous()调用的元素序号 |
1 | import java.util.*; |
集合接口Set
Set是一个不含重复元素的集合接口,它继承自Collection接口,并没有声明其他方法,它的方法都是从Collection接口继承来的。
Set集合中的对象不按特定的方式排序,只是简单地把对象加入集合中即可,但加入的对象一定不能重复。集合中元素的顺序与元素加入集合的顺序无关。
实现Set接口的两个主要类是哈希集合类HashSet及树集合类TreeSet。
哈希结合类HashSet(无序、不可重复、可为空)
哈希集合对所包含元素的访问并不是像线性表一样使用下标,而是根据哈希码来存取集合中的元素。
哈希码
:哈希集合是在元素的存储位置和元素的值k之间建立一个特定的对应关系f,使每个元素与一个唯一的存储位置相对应。因而在查找时,只要根据元素的值k,计算f(k)的值即可,如果此元素在集合中,则必定在存储位置f(k)上,因此不需要与集合中的其他元素进行比较便可直接取得所查的元素。称这个对应关系f为哈希函数,按这种关系建立的表称为哈希表
,也称散列表
。
HashSet根据哈希码
来确定元素在集合中的存储位置(即内存地址),因此可以根据哈希码来快速地找到集合中的元素。HashSet集合不保证迭代顺序,但允许元素值为null。
在比较两个加入哈希集合HashSet中的元素是否相同时,会先比较哈希码方法hashCode()的返回值是否相同,若相同则再使用equals()方法比较其存储位置(即内存地址),若两者都相同则视为相同的元素。
- HashSet <E>集合类的构造方法
构造方法 | 功能说明 |
---|---|
public HashSet() | 创建默认初始容量是16,默认上座率为0.75的空哈希集合 |
public HashSet(int initialCapacity) | 创建初始容量是initialCapacity,默认上座率为0.75的空哈希集合 |
public HashSet(int initialCapacity, float loadFactor) | 创建初始容量是initialCapacity,默认上座率为loadFactor的空哈希集合 |
public HashSet(Collection < ? extends E > c) | 创建包含容器c中所有元素,默认上座率为0.75的哈希集合 |
构造方法中的上座率也称装填因子,上座率的值为0.0~1.0表示集合的饱和度。当集合中的元素个数超过了容量与上座率的乘积,容量就会自动翻倍。
- HashSet <E>集合类的常用方法
构造方法 | 功能说明 |
---|---|
public boolean add(E e) | 如果集合中尚未包含指定元素,则添加元素e并返回true;如果集合中已包含该元素,则该调用不更改集合并返回false |
public void clear() | 删除集合中的所有元素,集合为空 |
public boolean contains(Object o) | 如果集合中包含元素o,则返回true |
public int size() | 返回集合中所包含元素的个数,即返回集合的容量 |
1 | import java.util.*; |
树集合类TreeSet(有序、不可重复、可为空)
树集合类TreeSet不仅实现了Set接口,还实现了java.util.SortedSet接口。
TreeSet的工作原理与HashSet相似,但TreeSet增加了一个额外步骤,以保证集合中的元素总是处于有序状态。
- TreeSet <E>类的构造方法
构造方法 | 功能说明 |
---|---|
public TreeSet() | 创建新的空树集合,其元素按自然顺序进行排序 |
public TreeSet(Collection < ? extends E > c) | 创建包含容器c元素的新TreeSet,按其元素的自然顺序进行排序 |
- TreeSet <E>类新增的方法
新增的方法 | 功能说明 |
---|---|
public E first() | 返回集合中的第一个(最低)元素 |
public E last() | 返回集合中的最后一个(最高)元素 |
public SortedSet < E > headSet(E toElement) | 返回一个新集合,新集合元素是toElement(不包含toElement)之前的所有元素 |
public SortedSet < E > tailSet(E toElement) | 返回一个新集合,新集合元素包含fromElement及fromElement之后的所有元素 |
public SortSet < E > subSet(E fromElement, E toElement) | 返回一个新集合,新集合包含从fromElement到toElement(不包含toElement)之间的所有元素 |
public E lower(E e) | 返回严格小于给定元素e的最大元素,如果不存在这样的元素,则返回null |
public E higher(E e) | 返回严格大于给定元素e的最小元素,如果不存在这样的元素,则返回null |
public E floor(E e) | 返回小于或等于给定元素e的最大元素,如果不存在这样的元素,返回null |
public E ceiling(E e) | 返回大于或等于给定元素e的最小元素,如果不存在这样的元素,则返回null |
映射接口Map
Map中的元素都是成对出现的,它提供了键(key)到值(value)的映射。
Map中键可以是任意类型的对象。
Map中检索一个元素,必须提供相应的键,这样就可以通过键访问到其对应元素的值。Map中的每个键都是唯一的,且每个键最多只能映射到一个值。
Map没有继承Collection接口。
- Map <K,V>接口的常用方法
常用方法 | 功能说明 |
---|---|
V put(K key, V value) | 以key为键,向集合中添加值为value的元素,其中key必须唯一,否则新添加的值会取代已有的值 |
void putAll(Map < ? extends K, ? extends V> m) | 将映射m中的所有映射关系复制到调用此方法的映射中 |
boolean containsKey(Object key) | 判断是否包含指定的键key |
boolean containsValue(Objext value) | 判断是否包含指定的值value |
V get (Object key) | 返回键key所映射的值,若key不存在则返回null |
Set < K > keySet() | 返回该映射中所有键对象形成的Set集合 |
Collection < V > values() | 返回该映射中所有值对象形成Collection集合 |
V remove(Object key) | 将键为key的条目,从Map对象中删除 |
Set < Map.Entry < K, v > > entrySet() | 返回映射中的键-值对的集合 |
映射接口Map常用的实现类有哈希映射HashMap和树映射TreeMap。
HashMap(键、值可为空)
HashMap映射是基于哈希表的Map接口的实现类,所以HashMap通过哈希码对其内部的映射关系进行快速查找,因此对于添加和删除映射关系效率较高,并且允许使用null值和null键,但必须保证键的唯一性。
- HashMap < K, V > 映射常用的构造方法
构造方法 | 功能说明 |
---|---|
public HashMap() | 构造一个具有默认初始容量(16)和默认上座率(0.75)的空HashMap对象 |
public HashMap(int initialCapacity) | 创建初始容量为initialCapacity 和默认上座率(0. 75)的空HashMap对象 |
public HashMap(Map < ? extends K, ? extends V > m) | 创建一个映射关系与指定Map相同的新HashMap对象。具有默认上座率(0.75)和足以容纳指定Map中映射关系的初始容量 |
TreeMap(有序,键不可为空,值可为空)
而树映射TreeMap中的映射关系存在一定的顺序,如果希望Map映射中的元素也存在一定的顺序,应该使用TreeMap类实现的Map映射,由于TreeMap类实现的Map映射中的映射关系是根据键对象按照一定的顺序排列的,因此不允许键对象是null。
- TreeMap <K,V>映射的构造方法
构造方法 | 功能说明 |
---|---|
public TreeMap() | 使用键的自然顺序创建一个新的空树映射 |
public TreeMap(Map < ? extends K, ? extends V > m) | 创建一个与给定映射具有相同映射关系的新树映射,该映射根据其键的自然顺序进行排序 |
- TreeMap <K, V > 映射的常用方法
常用方法 | 功能说明 |
---|---|
public K firstKey() | 返回映射中的第一个(最低)键 |
public K lastKey() | 返回映射中的最后一个(最高)键 |
public SortedMap < K, V > headMap(K toKey) | 返回键值小于toKey的那部分映射 |
public SortedMap < K, V> tailMap(K fromKey) | 返回键值大于或等于fromKey的那部分映射 |
public K lowerKey(K key) | 返回严格小于给定键key的最大键,如果不存在这样的键,则返回null |
public K floorKey(K key) | 返回小于或等于给定键key的最大键,如果不存在这样的键。则返回null |
public K higherKey(K key) | 返回严格大于给定键key的最小键,如果不存在这样的键,则返回null |
public K ceilingKey(K key) | 返回大于或等于给定键key的最小键,如果不存在这样的键,则返回null |
本章小结
- 在定义类、接口或方法时若指定了“类型参数”,则分别称为泛型类、泛型接口或泛方法。
- 用泛型类创建的泛型对象就是在泛型类体内的每个类型参数T处分别用某个具体的实际类型替代,这个过程称为泛型实例化,利用泛型类创建的对象称为泛型对象。
- 在创建泛型类对象的过程中,实际类型必须是引用类型,而不能用基本类型。
- 泛型方法与其所在的类是否是泛型类没有关系。
- 在调用泛型方法时,可以将实际类型放在尖括号内作为方法名的前缀。
- 泛型方法的返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的。泛型方法广泛应用在方法返回值和参数均是容器类对象的情况。
- 泛型方法与泛型类之间的一个重要区别是:对于泛型方法,不需要把实际的类型传递给泛型方法;但泛型类却恰恰相反,即必须把实际的类型参数传递给泛型类。
- 虽然泛型的类型参数代表一种数据类型,但不能使用泛型的类型参数创建对象。
- 在泛型中可以用类型参数声明一个数组,但不能使用类型参数创建数组对象。
- 不能在静态环境中使用泛型类的类型参数。
- 异常类不能是泛型的,即在异常类中不能使用泛型的类型参数。
- 在定义泛型类或使用泛型类创建对象时,对泛型的类型做出限制称为泛型限制。
- 泛型类的通配符有三种形式:第1种是”?”,它等价于”? extends Object”,称为非受限通配;第2种是”? extends T”,表示T或T的一个未知子类型,称为上限通配;第3种是”? super T”,表示T或T的一个未知父类型,称为下限通配。
- 当方法中的多个参数之间或参数与返回值之间存在类型依赖关系时,则应选用泛型方法。如果方法中不存在类型之间的依赖关系,则应选用通配符。
- 容器是存储对象的数据结构的集合。容器框架中定义的所有接口和类都存储在java.util包中。
- 从容器的当前元素获取其后续元素进行访问的过程称为迭代,迭代也称为遍历。
- List的对象用于存储一个由元素构成的线性表;Set的对象是存储一组不重复的元素集合;Map的对象保持了键到值的映射。
- List是一种包含有序元素的线性表,其中的元素必须按顺序存放,且可重复,也可以是空值null。实现List接口的类主要有链表类LinkedList和数组列表类ArrayList。
- LinkedList是实现List接口的链表类,采用双向链表结构保存元素,访问元素的时间取决于元素在表中所处的位置,但对链表的增长或缩小则没有任何额外的开销。
- ArrayList是实现List接口的数组列表类,它使用一维数组实现List,支持元素的快速访问,但在数组的扩展或缩小时则需要额外的系统开销。
- Set是一个不含重复元素的集合接口。实现Set接口的两个主要类是哈希集合类HashSet及树集合类TreeSet。
- HashSet的工作原理是在哈希集合中元素的“值”与该元素的存储位置之间建立起一种映射关系,这种映射关系称为哈希函数或散列函数,由哈希函数计算出来的数值称为哈希码或散列索引。虽然HashSet中的元素是无序的,但由于HashSet特性还是可以快速地添加或访问其中的元素。
- 因为对不同元素计算出的哈希码可能相同,所以判断哈希集合中的元素是否相同时需要同时使用hashCode()方法和equals()方法。
- TreeSet类对象中的元素总是有序的,所以当插入元素时需要一定的开销。
- Map中的元素都是成对出现的,它提供了键(key)到值(value)的映射。
- 映射接口Map常用的实现类有HashMap和TreeMap。HashMap类与TreeMap类的关系如同HashSet与TreeSet的关系一样。
- HashMap类是基于哈希表的Map接口的实现,允许使用null值和null键,但必须保证键的唯一性,HashMap是无序的。
- TreeMap类中的映射关系存在一定的顺序,不允许键对象是null。TreeMap是有序的。
课后习题
- 什么是泛型的类型参数?泛型的主要优点是什么?在什么情况下使用泛型方法?泛型类与泛型方法传递类型实参的主要区别是什么?
- 已知Integer是Number的子类,GeneralType<Integer>是GeneralType<Number> 的 子 类 吗 ? GeneralType<Object> 是GeneralType<T>的父类吗。
- 在泛型中,类型通配符的主要作用是什么?
- 分别简述LinkedList与ArrayList、HashSet与TreeSet、HashMap与TreeMap有何异同。
- 将1~10的整数存放到一个线性表LinkedList的对象中,然后将其下标为4的元素从列表中删除。
1 | import java.util.*; |
- 利用ArrayList类创建一个对象,并向其添加若干个字符串型元素,然后随机选一个元素输出。
- 已知集合A={1,2,3,4}和B={1,3,5,7,9,11},编程求A与B的交集、并集和差集。
1 | import java.util.*; |
- 利用随机函数生成10个随机数,并将它们存入到一个HashSet对象中,然后利用迭代器输出。
1 | import java.util.*; |
- 利用随机函数生成10个随机数,并将它们有序地存入到一个TreeSet对象中,然后利用迭代器有序地输出。
1 | import java.util.*; |
- 利用HashMap类对象存储公司电话号码簿,其中包含公司的电话号码和公司名称,然后进行删除一个公司和查询一个公司的操作。
第十三章 注解、反射、内部类、匿名内部类、Lambda表达式
注解
是代码里的特殊标记,用来告知编译器要做什么事情;反射
允许程序在运行状态时,可以对任意一个字节码(.class文件)获取它的所有信息;内部类
是定义在类中的嵌套类;匿名内部类
则是在定义类的同时就创建该类的一个对象;Lambda表达式
可以被看作是使用精简语法的匿名内部类,编译器对待一个Lambda表达式如同它是从一个匿名内部类创建的对象。
注解Annotation
注解Annotation与类、接口、枚举在同一个层次。
它其实就是程序代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取并执行相应的处理。注解主要用于告知编译器要做什么事情,在程序中可对任何程序元素进行注解。注解可以声明在包、类、成员变量、成员方法、局部变量、方法参数等的前面,用来对这些程序元素进行说明、注释。
java.lang.annotation.Annotation是注解接口。
1 | @注解名 |
基本注解
@Deprecated
该注解用于表示某个程序元素(如类、方法等)已过时,不建议使用。
@Override
该注解只用于方法,用来限定必须覆盖父类中的方法,主要作用是保证方法覆盖的正确性。
@SuppressWarinings
抑制警告信息的出现,即不允许出现警告信息。
该注解可以用于类型、构造方法、成员方法、成员变量、参数以及局部变量等 。
1 |
- @SuppressWarnings中的警告参数
警告参数 | 功能说明 |
---|---|
deprecation | 忽略使用了不建议使用的程序元素时所产生的警告 |
unchecked | 忽略未经检查的类型转换所产生的警告 |
boxing | 忽略装箱/拆箱操作所产生的警告 |
fallthrough | 忽略switch语句中没有使用break时所产生的警告 |
path | 忽略在源文件路径、类路径中有不存在的路径时所产生的警告 |
serial | 忽略实现Serializable接口但没有定义serialVersionUID常量时所产生的警告 |
unused | 忽略程序元素已被定义但从未使用所产生的警告 |
rawtypes | 忽略因使用泛型但未限制类型时所产生的警告 |
finally | 忽略finally子句不能正常完成时所产生的警告 |
all | 忽略所有警告 |
@SafeVarargs
用于抑制堆污染(将一个不带泛型的对象赋值给带泛型的对象,将导致泛型对象污染)警告。
抑制堆污染警告三种方式:
- 使用
@SafeVarargs
注解修饰引发该警告的方法,该方式是专门抑制堆污染警告而提供的,也是推荐使用的方式 - 使用
@SuppressWarnings("unchecked")
注解修饰 - 编译时使用
-Xlint:varargs
选项
@FunctionalInterfase
指定某个接口必须是函数式接口,如果一个接口中只有一个抽象方法,则该接口称为函数式接口。@FunctionalInterfase注解只能用于修饰函数式接口,不能用于修饰程序的其他元素。
函数式接口是为Lambda表达式准备的,所以允许使用Lambda表达式来创建函数式接口的实例。
元注解
元注解也称元数据注解,是对注解进行标注的注解。
@Target
限制注解的使用范围,即指定该注解可用于哪些程序元素。
1 |
- 枚举
java.lang.annotation.ElementType
中表示范围的主要枚举值
作用范围 | 功能说明 |
---|---|
CONSTRUCTOR | 只能用在构造方法的声明中 |
FIELD | 只能用在成员变量声明上 |
LOCAL_VARIABLE | 只能用在局部变量声明上 |
METHOD | 只能用在方法声明上 |
PACKAGE | 只能用在包的声明上 |
PARAMETER | 只能用在参数的声明上 |
TYPE | 只能用在类、接口或枚举类型的声明上 |
ANNOTATION_TYPE | 只能用在注解声明上 |
@Retention
说明注解的保存范围,保存范围使用枚举类型java.lang.annotation.RetentionPolicy
来指定其保留策略值。
1 |
- 枚举java.lang.annotation.RetentionPolicy中的注解保留策略值
保存策略值 | 功能说明 |
---|---|
SOURCE | 注解只存在于源代码文件(.java)中,在编译后不会保存在类文件(.class)中 |
CLASS | 在编译时将注解保存在字节码文件中,即编译器把注解记录在. class文件中。当运行Java程序时,JVM不会加载此注解信息。若没指定范围,则此为默认值 |
RUNTIME | 编译器把注解记录在. class文件中。当运行Java 程序时,JVM会加载注解信息,并可以通过反射获取注解信息 |
@Document
指定被修饰的注解可被javadoc.exe工具提取成文档。
定义类时使用@Document注解进行修饰,则所有使用该注解修饰的程序元素的API文档中将包含该注解说明。
@Inherited
描述一个父类的注解可以被子类所继承。
如果一个注解需要被其子类所继承,则在声明时直接使用@Inherited注解就行。
@Repeatable
开发重复注解。
允许使用多个相同类型的注解来修饰同一程序元素,只要在定义注解时使用@Repeatable元注解来进行修饰。
类型注解
在使用类型的地方指定注解,可以注解方法的返回值、方法内this的类型等。
类型注解可以用在任何用到类型的地方。除了在定义类、接口、方法和成员变量等常见的程序元素时可以使用类型注解外,还可以在创建对象、方法参数、类型转换、使用throws声明抛出异常、使用implements实现接口等位置使用类型注解。
1 |
自定义注解
1 | [public] 注解名 { |
注解的成员由未实现的方法组成,其中的成员变量以无参数方法的形式来声明,即变量名后面必须有圆括号”()”,名称和返回值数据类型定义了该成员变量的名字和类型。
注解中的成员将在使用时进行实现,也可以在声明时使用default关键字来指定变量的初始值。
反射机制
Java中有许多对象在运行时都会出现两种类型:编译时类型
和运行时类型
。
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。
Class类
Class类的对象表示正在运行的Java程序中的类或接口,也就是任何一个类被加载时,即将类的.class文件(字节码文件)读入内存的同时,都自动为之创建一个java.lang.Class对象。
- Class类的常用方法
常用方法 | 功能说明 |
---|---|
public Package getPackage() | 返回Class对象所对应类的存放路径 |
public static Class < ? > forName(String className) | 返回名称为className的类或接口的Class对象 |
public String getName() | 返回Class对象所对应类的”包.类名”形式的全名 |
public Class < ? super T > getSuperclass() | 返回Class对象所对应类的父类的Class对象 |
public Class < ? >[] getInterfaces() | 返回Class对象所对应类所实现的所有接口 |
public Annotation[] getAnnotations() | 以数组的形式返回该程序元素上的所有注解 |
public Constructor < T > getConstructor(Class < ? > … parameterTypes) | 返回Class对象所对应类的指定参数列表的public构造方法 |
public Constructor < ? >[] getConstructors() | 返回Class对象所对应类的所有publie构造方法 |
public Constructor < T > getDeclaredConstructor(Class < ? > … parameterTypes) | 返回Class对象所对应类的指定参数列表的构造方法。与访问权限无关 |
public Constructor < ? >[] getDeclaredConstructor | 返回Class对象所对应类的所有构造方法,与访问权限无关 |
public Field getField(String name) | 返回Class对象所对应类的名为name的public成员变量 |
public Field[] getFields() | 返回Class对象所对应类的所有public成员变量 |
public Field[] getDeclaredFields() | 返回Class对象所对应类的所有成员变量,与访问权限无关 |
public Method getMethod(String name, Class < ? > … parameterTypes ) | 返回Class对象所对应的指定参数列表的public方法 |
public Method[] getMethods() | 返回Class对象所对应类的所有public成员方法 |
public Method[] getDeclaredMethods() | 返回Class对象所对应类的所有成员方法,与访问权限无关 |
通过getFields()和getMethods()方法获得权限为public成员变量和成员方法时,还包括从父类继承得到的成员变量和成员方法;
而通过getDeclaredFields()和getDeclaredMethods()方法只是获得在本类中定义的所有成员变量和成员方法。
被载入JVM的类都有一个唯一标识就是该类的全名,即包括包名和类名。
在Java中程序获得Class对象有如下3种方式
- 使用Class类的静态方法forName
1 | Class cObj = Class.forName("java.lang.String"); |
forName()方法声明抛出ClassNotFoundException
异常,因此调用该方法时必须捕获或抛出该异常。
- 用类名调用该类的class属性来获得该类对应的Class对象
1 | Class < Cylinder > cObj = Cylinder.class; |
- 用对象调用getClass()方法来获得该类对应的Class对象
1 | Class cObj = per.getClass(); |
通过类的class属性获得该类所对应的Class对象,会使代码更安全,程序性能更好,因此大部分情况下建议使用第二种方式。但如果只获得一个字符串,例如获得String类对应的Class对象,则不能使用String.class方式,而是使用Class.forName(“java.lang.String”)。
反射包reflect中的常用类
反射机制中除了上面介绍的java.lang包中的Class类之外,还需要java.lang.reflet包中的Constructor类、Method类、Field类和Parameter类。
Executable抽象类派生了Constructor和Method两个子类。
Executable抽象类
- java.lang.reflect.Executable类的常用方法
常用方法 | 功能说明 |
---|---|
public Parameter[] getParameters | 返回所有形参,存人数组Parameter[]中 |
public int getParameterCount() | 返回参数的个数 |
public abstract Class < ? > [] getParameterTypes() | 按声明顺序以Class数组的形式返回各参数的类型 |
public abstract int getModifiers() | 返回整数表示的修饰符public、protected.private、final、static、abstract等关键字所对应的常量 |
public boolean isVarArgs() | 判断是否包含数量可变的参数 |
getModifiers()方法返回的是以整数表示的修饰符。此时引入Modifier类,通过调用Modifier.toString(int mod)方法返回修饰符常量所应的字符串。
Constructor类
java.lang.reflect.Constructor<T>类是java.lang.reflect.Executable类的直接子类,用于表示类的构造方法。通过Class对象的getConstructors()方法可以获得当前运行时类的构造方法。
- java.lang.reflect.Constructor <T>类的常用方法
常用方法 | 功能说明 |
---|---|
public String getName() | 返回构造方法的名字 |
public T newInstance(Object… initargs) | 通过该构造方法利用指定参数列表创建一个该类的对象,如果未设置参数则表示采用默认无参的构造方法 |
public void setAccessible(boolean flag) | 如果该构造方法的权限为private,默认不允许通过反射利用new Instance()方法创建对象。如果先执行该方法,并将入口参数设置为true, 则允许创建 |
通过Class对象的getConstructors()方法可以获得当前运行时类的构造方法。
Method类
java.lang.reflect.Method类是java.lang.reflect.Executable类的直接子类。
封装成员方法的信息,调用Class对象的getMethod()方法或getMethods()方法可以获得当前运行时类的指定方法或所有方法。
- java.lang.reflect.Method类的常用方法
常用方法 | 功能说明 |
---|---|
public String getName() | 返回方法的名称 |
public Class < ? > getReturnType() | 以Class对象的形式返回当前方法的返回值类型 |
public Object invoke(Object obj,Object… args) | 利用给定参数列表执行指定对象obj中的该方法 |
Field类
java.lang.reflect.Field类用于封装成员变量信息,调用Class对象的getField()方法或getFields()可以获得当前运行时类的指定成员变量或所有成员变量。
- java.lang.reflect.Field类的常用方法
常用方法 | 功能说明 |
---|---|
public String getName() | 返回成员变量的名称 |
Xxx getXxx() | 返回成员变量的值,其中Xxx代表基本类型,如果成员变量是引用类型,则直接使用get(Object obj)方法 |
void setXxx(Object obj,Xxx val) | 设置成员变量的值,其中Xxx代表基本类型,如果成员变量是引用类型,则直接使用set(Object obj,Object val)方法 |
public Class < ? > getType() | 返回当前成员变量的类型 |
Parameter类
java.lang.reflect.Parameter类是参数类,每个Parameter对象代表方法的一个参数。
- java.lang.reflect.Parameter类的常用方法
常用方法 | 功能说明 |
---|---|
get int getModifiers() | 返回参数的修饰符 |
public String getName() | 返回参数的形参名 |
public Type getParameterizedType() | 返回带泛型的形参类型 |
public Class < ? > getType() | 返回形参类型 |
public boolean isVarArgs() | 判断该参数是否为可变参数 |
public boolean isNamePresent() | 判断. class文件中是否包含方法的形参名信息 |
反射的应用
1 | import java.lang.reflect.Constructor; |
1 | 构造方法名:Person,参数:java.lang.String,参数:int |
内部类与匿名内部类
内部类
(inner class)是定义在类中的类,其主要作用是将逻辑上相关的类放到一起;匿名内部类
(anonymous inner class)是一种特殊的内部类,它没有类名,在定义类或实现接口的同时,就生成该类的一个对象,由于不会在其他地方用到该类,所以不用取名字,因而被称为匿名内部类。
内部类
内部类是包含在类中的类,所以内部类也称为嵌套类
,包含内部类的类称为外部类
。
其实内部类可以看作是外部类的一个成员,所以内部类也称为成员类
。
内部类在编译完成之后,所产生的文件名称为”外部类名$内部类名.class”
“Out.class Out$Student.class”
在内部类对象中保存了一个对外部类对象的引用,当在内部类的成员方法中访问某一变量时,如果在该方法和内部类中都没有定义过这个变量,调用就会被传递给内部类中保存的那个对外部类对象的引用。
- 内部类可以声明为private或protected。
- 内部类的前面用final修饰,则表明该内部类不能被继承。
- 内部类可以定义为abstract,但需要被其他的内部类继承或实现。
- 内部类名不能与包含它的外部类名相同。
- 内部类也可以是一个接口,该接口必须由另一个内部类来实现。
- 内部类不但可以在类中定义,也可以在程序块之内定义。例如,在方法中或循环体内部都可以定义内部类。但是方法中定义的内部类只能访问方法中的final类型的局部变量。
- 内部类既可以访问外部类的成员变量,包括静态和实例成员变量,也可以访问内部类所在方法的局部变量。
- 内部类如果被声明为static,则静态内部类将自动转化为”顶层类”(toplevel class),即它没有父类,而且不能引用外部类的成员或其他内部类中的成员。非静态内部类不能声明静态成员,只有静态内部类才能声明静态成员。
匿名内部类
某个类的对象只使用一次,则可以将类的定义与对象的创建在一步内完成,即在定义类的同时就创建该类的一个对象,以这种方式定义的类不用取名字,所以称为匿名内部类(anonymous inner class)
1 | new TypeName() { //()内不能有参数 |
匿名内部类可以继承一个类或实现一个接口,其中TypeName是匿名内部类所继承的类或实现的接口。
实现一个接口,则该类是Object类的直接子类。匿名内部类继承一个类或实现一个接口不需要使用extends或implements关键字。
在创建匿名内部类时,其实是调用其父类的无参构造方法来实现的。
所以匿名内部类既是一个内部类也是一个子类,不可能用匿名内部类声明对象。
匿名内部类名前不能有修饰符,也不能定义构造方法,因为它没有名字,也正是这个原因,在创建对象时也不能带参数。
- 匿名内部类返回的是一个对象的引用
1 | TypeName obj = new TypeName() { |
- 可以将创建的匿名内部类对象作为方法调用的参数
1 | someMethod(new TypeName() { |
- 弥补内部类里没有定义到的方法
1 | static class Inner { |
匿名内部类在编译完成之后,所产生的文件名称为”外部类名$编号.class”,其中编号为1,2,…,n,每个编号为i的文件对应于第i个匿名内部类。
“App13_4.class”、”App13_4$Inner.class”、”App13_4$1.class”
- 匿名内部类必须是继承一个父类或实现一个接口,但不能使用extends或implements关键字。
- 匿名内部类总是使用它父类的无参构造方法来创建一个实例。如果匿名内部类实现一个接口,调用的构造方法是Object()。
- 匿名内部类可以定义自己的方法,也可以继承父类的方法或覆盖父类的方法。
- 匿名内部类必须实现父类或接口中的所有抽象方法。
- 使用匿名内部类时,必然是在某个类中直接使用匿名内部类创建对象,所以匿名内部类一定是内部类,匿名内部类可以访问外部类的成员变量和方法。
- 匿名内部类中不能声明static成员变量和static成员方法。
利用接口创建匿名内部类对象并实现接口中抽象方法。
1 | interface IShape { |
函数接口和Lambda表达式
Lambda表达式指的是应用在只含有一个抽象方法的接口环境下的一种简化定义形式,可用于解决匿名内部类的定义复杂问题。
函数式接口
函数式接口(Functional Interface,FI)是指只包含一个抽象方法的接口,因此也称为单抽象方法接口。
每一个Lambda表达式都对应一个函数式接口,可以将Lambda表达式看作是实现函数式接口的匿名内部类的一个对象。
1 |
|
函数式接口只能有一个抽象方法需要被实现,但有如下特殊情况的除外:
- 函数式接口中可以有Object类中覆盖的方法,也就是equals()、toString()、hashcode()等方法。
- 函数式接口中只能声明一个抽象方法,但是静态方法和默认方法(即用default修饰的方法)不属于抽象方法,因此可以在函数式接口中定义静态方法和默认方法。
Lambda表达式
Lambda表达式是可以传递给方法的一段代码。
可以是一条语句,也可以是一个代码块,因不需要方法名,所以说Lambda表达式是一种匿名方法,即没有方法名的方法。
任何Lambda表达式必定有对应的函数式接口。
可以使用Lambda表达式创建一个与匿名内部类等价的对象,看做使用精简语法的匿名内部类。
1 | (类型1 参数1, 类型2 参数2, ...)->{方法体} |
- 参数列表中的参数都是匿名方法的形参,即输入参数。
->
是Lambda运算符- 方法体可以是单一的表达式或由多条语句组成的语句组。
- 如果Lambda表达式需要返回值,且方法体中只有一条省略了return关键字的语句,则Lambda表达式会自动返回该条语句的结果值。
- 如果Lambda表达式没有参数,可以只给出圆括号。
- 如果Lambda表达式只有一个参数,并且没有给出显式的数据类型,则圆括号可以省略。
1 |
|
Lambda表达式只适用于包含一个抽象方法的接口,对于包含有多个抽象方法的接口,编译器则无法编译Lambda表达式。
如果是单参数又无须写出参数类型时,圆括号()也可省略。若方法有返回值,且方法体只有一条return语句,则Lambda表达式中的return关键字也可省略。
Lambda表达式所实现的匿名方法则是在函数式接口中声明的。
Lambda表达式可以作为表达式、方法参数和方法返回值。
Lambda表达式作为方法的参数
接受Lambda表达式的参数必须是与该Lambda表达式兼容的函数式接口类型。
1 |
|
在java.util.function
包中定义了大量函数式接口,如功能型接口Function<T,R>
和BiFunction<T,U,R>
、断言型接口Predicate<T>
、供给型接口Supplier<T>
和消费型接口Consumer<T>
等,它们使编写Lambda表达式更加容易。
方法引用
双冒号::
运算符用于方法引用。
方法都只带有一个函数式接口对象作为其参数。Lambda表达式可能仅仅调用一个已经存在的方法,如果传递的表达式有实现的方法,可以使用方法引用来代替Lambda表达式。
1 | //用对象名引用实例方法 |
方法名后边不能括号,右边只能有一个new字符。
有重载方法时,JVM会根据参数的个数与类型来判断并调用相应的方法。同Lambda表达式类似,方法引用也不会单独存在,总是会转换为函数式接口的实例。
方法引用是Lambda表达式的一种特例。
方法引用的唯一用途就是支持Lambda表达式的简写。
实例方法名引用
1 | //匿名内部类方式 |
静态方法名引用
1 |
|
类名实例方法名引用
1 | str1.compareTo(str2); |
类名new引用
构造方法的引用赋值给与构造方法具有相同方法头的任何函数式接口对象。
1 |
|
方法引用就相当于为方法定义了别名,已经存在的方法相当于提供了对函数式接口中抽象方法的实现。
本章小结
- 注解(annotation)也?元数据,所谓元数据就是用来描述数据的数据。
- 注解的语法格式是“@注解名”。根据注解的作用可以将注解分为基本注解、元数据注解(或称元注解)与自定义注解三种。
- 反射机制允许Java程序在运行时动态获得所需类的内部信息及动态调用对象方法的功能。
- 在Java程序中获得Class对象有三种方式:一是使用Class类的静态方法forName();二是用类名调用该类的class属性来获得该类对应的Class对象,即”类名.class”;三是用对象调用getClass()方法来获得该类对应的Class对象,即”对象.getClass()”。
- 内部类是定义在类中的类;而匿名内部类是一种特殊的内部类,它没有类名,在定义类的同时,就生成该类的一个对象,由于不会在其他地方用到该类,所以不用命名。
- 匿名内部类不能同时继承一个类又实现一个接口,也不能实现多个接口。
- 匿名内部类的好处是可利用内部类创建不具名称的对象,并利用它访问到类里的成员。
- 函数式接口是指只包含一个抽象方法的接口。
- Lambda表达式可以被看作是使用精简语法的匿名内部类。
- Lambda表达式适用于只包含一个抽象方法的函数式接口。
- 用Lambda表达式简化匿名内部类的方法就是去掉接口名和方法名等冗余信息,只保留方法的参数和方法体。
- 方法引用其实就是Lambda表达式的另外一种表现形式。
- 方法引用就相当于为方法定义了别名。
- 如果传递的表达式有实现的方法,则可以使用方法引用来代替Lambda表达式。
课后习题
- 什么是注解?根据注解的作用,注解分几种?
- 编写一个Java程序,使用JDK的基本注解,对覆盖方法使用@Override,再对另一方法使用@Deprecated。
- 反射的作用是什么?
- 编写具有反射功能的Java程序时,可使用哪三种方式获取指定类的Class对象?
- 内部类的类型有几种?分别在什么情况下使用?它们所起的作用有哪些?
- 内部类与外部类的使用有何不同?
- 怎样创建匿名内部类对象?
- 什么是Lambda表达式?Lambda表达式的语法是什么样?
- 什么是函数式接口?为什么Lambda表达式只适用于函数式接口?
- Lambda表达式与匿名内部类有什么样的关系?函数式接口为什么重要?
- Java定义了哪四种方法引用方式?对方法引用有什么要求?