文章目录
java核心技术 基础知识<第8-9章>
泛型的经典应用即集合。
8 泛型程序设计
Generic programming。
8.1 为什么要使用泛型程序设计
泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。和C++的模板很像。
类型参数的魅力在于:使得程序具有更好的可读性和安全性。
ArrayList<String> files = new ArrayList<>();
Java 语言的设计者发明了一个具有独创性的新概念, 通配符类型( wildcard type)。
凭经验来说, 那些原本涉及许多来自通用类型(如Object 或Comparable 接口)的强制类型转换的代码一定会因使用类型参数而受益。
8.2 定义简单泛型类
默认命名方式:
<E> // element集合元素
<K, V> // key value,键值
<T, U, S> // 任意类型
public class Pair<T, U> {
private T first; // 使用T代替某种类型
}
C++ 注释: 从表面上看, Java 的泛型类类似于C++ 的模板类。唯一明显的不同是Java 没有专用的template 关键字。但是, 在本章中读者将会看到, 这两种机制有着本质的区别。
8.3 泛型方法
注意,类型变量放在修饰符(这里是public static ) 的后面,返回类型的前面。
当调用一个泛型方法时,在方法名前的尖括号中放人具体的类型。
public static <T> T getMiddle(T... a)
{
return a[a.length / 2];
}
String middle = ArrayAlg.<String>getMiddle("JohnM", "Q.n", "Public");
大多数情况,编译器可以推导出使用哪个T。
提示:如果想知道编译器对一个泛型方法调用最终推断出哪种类型, PetervonderAM 推荐了这样一个窍门: 有目的地引入一个错误, 并研究所产生的错误消息。
C++注释:C++把类型参数放在方法名后面,可能有歧义。g(f<a, b>©)可能理解为bool值f<a和b>c调用g。所以java在方法名前面。
8.4 类型变量的限定
对类型加以约束。比如必须实现某个接口,必须派生于某个超类。
public static <T extends Comparable & Serializable> T min(T[] a){
...
}
限定类型用“ &” 分隔,而逗号用来分隔类型变量。在Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类。如果用
一个类作为限定,它必须是限定列表中的第一个。
8.5 泛型代码和虚拟机
类型擦除:
泛型实现原理:虚拟机对类型进行了擦除,作为一个普通类。
无限定的类型用Object替换。有限定的类型用第一个限定的类型变量来替换,其余用类型转换。所以要把tagging接口放在最后一个。
C++注释:就这点而言, Java 泛型与C++ 模板有很大的区别。C++ 中每个模板的实例化产生不同的类型,这一现象称为**“ 模板代码膨胀”**。Java 不存在这个问题的困扰。
翻译泛型表达式:
调用泛型方法时,编译器插入强制类型转化。因为被擦除了,所以编译器要加上强制类型转化。
翻译泛型方法:
用桥接方式。
总之,需要记住有关Java 泛型转换的事实:
• 虚拟机中没有泛型, 只有普通的类和方法。
• 所有的类型参数都用它们的限定类型替换。
• 桥方法被合成来保持多态。
• 为保持类型安全性,必要时插人强制类型转换。
8.6 约束与局限性
- 不能用基本类型实例化类型参数
没有Pair, 只有Pair。 - 运行时类型查询只适用于原始类型
getClass总会返回原始类型。
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> stringPair = . .
Pair< Employee>employeePair = . .
if (stringPair.getClass() == employeePair.getClass()) // they are equal
- 不能创建参数化类型的数组
不能实例化参数化类型的数组, 例如:
Pair<String>[] table = new Pair<String>[10] ; // Error
- Varargs 警告
- 不能实例化类型变置
public Pair() { first = new T(); second = new T(); } // Error
- 不能构造泛型数组。类型擦除会让他永远构造Comparable[2]数组
T[] mm = new T[2];
- 泛型类的静态上下文中类型变量无效。被擦除了。
- 不能抛出或捕获泛型类的实例
8.7 泛型类型的继承规则
无论S与T有什么联系,通常,Pair<S>
与Pair<T>
没什么联系。
8.8 通配符类型
允许参数类型变化。例如:
Pair<? extends Employee>
类型Pair<Manager>
是Pair<? extends Employee>
的子类型。
? extends XX表示XX某一个具体的子类。
可以有 ? extends Employee getFirst()
但是不能有 void setFirst(? extends Employee)
。
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo) ;
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEnployee) ; // compile-time error
这样将不可能调用setFirst 方法。编译器只知道需要某个Employee 的子类型, 但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。!!wildcardBuddies可以接收任一子类,两个子类之间是不能转换的!!
将getFirst 的返回值赋给一个Employee 的引用完全合法。
通配符的超类型限定(superType bound):
? super Manager
限制为Manager的所有超类型。带有超类型限定的通配符的行为与8.8 节介绍的相反。可以为方法提供参数, 但不能使用返回值。例如, Pair<? super Manager> 有方法
void setFirst(<? super Manager>)
<? super Manager> getFirst()
编译器无法知道set的具体类型(任意Manager的超类,比如Object比如Employee)。因此只能传递Manager。
调用get,不能保证返回值类型,只能赋值给Object。
直观地讲,带有超类型限定的通配符(super)可以向泛型对象写入,带有子类型限定(extends)的通配符可以从泛型对象读取。
无限定通配符:
Pair<?>。返回值只能赋值给object,set方法不能调用。在有些简单的操作中比较有用,不需要实际类型的场景。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。
8.9 反射和泛型
泛型Class类,String.class实际上是Class类的对象(唯一对象)。
java.lang.Class中定义了很多对类型进行操作的函数。
使用Class参数进行类型匹配:
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException
{
return new Pair<>(c.newInstance() , c.newInstance()) ;
}
// 如果调用
makePair(Employee.class)
makePair一定返回Pair因为Employee.class是Class的一个对象。
9 集合
一堆用泛型实现的数据结构
9.1 Java集合框架
将集合的接口与实现分离:
interface和implementation分离,例如Queue:
接口定义:
public interface Queue<E> // a simplified form of the interface in the standard library
{
void add(E element) ;
E remove();
int size();
}
实现方式有两种(举例,实际并没有)
CircularArrayQueue<E>
循环队列。linkedListQueue<E>
链表
注:API文档中有一些AbstractXX的类,扩展这些类比实现Queue接口中的所有方法轻松。
Collection接口:
集合类的基本接口是Collection接口:
public interface Collection<E>{
boolean add(E element);
Iterator<E> interator();
...
}
add 方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true, 如果集合没有发生变化就返回false。
iterator返回一个实现Iterator接口的对象,可以依次访问集合中的元素。
迭代器:
Iterator 接口包含4 个方法:
public interface Iterator<E>
{
E next(); // 返回一个元素,迭代器向后
boolean hasNext(); // 有其他元素
void remove(); // 将会删除上次调用next 方法时返回的元素
// 删除指定元素时,要越过这个元素
default void forEachRemaining(Consumer<? super E> action) ;
}
forEachRemainging的用法:lambda表达式作用每个元素。
iterator.forEachRemaining(element -> do something with element) ;
for (String element : c) // 遍历所有元素
{
do something with element
}
应该将Java 迭代器认为是位于两个元素之间。当调用next 时, 迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
C++注释:与C++不同,C++的迭代器是元素本身。Java的查找与位置变更是紧密相连的。
泛型实用方法:
Collection接口声明了很多有用的方法
int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll (Collection<?> c)
boolean equals(Object other)
boolean addAll (Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll(Collection<?> c)
void clear()
boolean retainAll(Collection<?> c)
Object[] toArray()
<T> T[] toArray(T[] arrayToFill )
default boolean removelf (Predicate<? super E> filter) // 删除某种元素
Collection通过AbstractCollection实现了大部分通用的功能。是一种通过集成减少重复代码的手段。
集合框架中的接口:
Collection:
boolean add(E element)
Map:
V put(K key, V Value)
V get(K key)
List:
void add(int index, E element) // 增
void remove(int index) // 删
E set(int index, E element) // 改
E get(int index) // 查
ListIterator extends Iterator:
void add(E element) // 在迭代器前增一个元素
RandomAccess: // 标记接口,测试是否支持高效随机访问
if (c instanceof RandomAccess){
// 随机访问
}else{
// 逐个访问
}
Set == Collection,
// add不允许重复。
// equals意义不一样
NavigableSet, NavigableMap:
// 用于搜索和遍历有序集
9.2 具体的集合
Collection:
ArrayList 一种可以动态增长和缩减的索引序列
LinkedList 一种可以在任何位置进行高效地插人和删除操作的有序序列
ArrayDeque 一种用循环数组实现的双端队列
HashSet 一种没有重复元素的无序集合
TreeSet 一种有序集
EnumSet 一种包含枚举类型值的集
LinkedHashSet 一种可以记住元素插人次序的集
PriorityQueue 一种允许高效删除最小元素的集合
Map:
HashMap 一种存储键/ 值关联的数据结构
TreeMap 一种键值有序排列的映射表
EnumMap 一种键值属于枚举类型的映射表
LinkedHashMap 一种可以记住键/ 值项添加次序的映射表
WeakHashMap 一种其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap 一种用=而不是用equals 比较键值的映射表
链表:
双向链表。
是有序集合,每个对象位置很重要。
list可以produce ListIterator,可使用ListIterator的add向链表某个迭代器前add元素。
ListIterator可从两个方向遍历。previous(),hasPrevious()
list.listIterator(n)将返回索引为n的元素的前面的位置。
list.get(n) 效率极低。
add 方法只依赖于迭代器的位置, 而remove 方法依赖于迭代器的状态(两个方向指针行为不一样)。
链表结构被改后,其他迭代器会失效。
可使用toString和contains方法。
相关类,接口:
java.util.List<E> // 继承Collection
java.util.ListIterator<E> // 生成自List
java.util.LinkedList<E> // List的具体实现
数组列表:
实现List接口(有序,位置重要的集合)。
ArrayList(线程不安全,单线程快),Vector(线程安全, 单线程慢)。
散列集:
元素位置无所谓,可快速查找元素。
散列表(hash table)。hash取模,链表,平衡二叉树。
如果自定义类,就要负责实现这个类的hashCode方法。equals结果为true,则两个对象的hashCode必须相同。
Java SE 8中,散列表桶满之后会从链表变为平衡二叉树。如果选择散列函数不当,会产生很多冲突。
HashSet extends AbstractSet extends Set extends Collection
访问随机!contains效率高!
java.util.HashSet<E>
// 可构造自其它Collection,可指定容量和装填因子。
树集:
TreeSet是有序的。遍历是按照排序后顺序的。红黑树实现。
相比HashSet,增删查要慢一些.logn复杂度。
元素要实现Comparable
接口或提供Comparator
TreeSet extends AbstractSet&SortedSet extends Set extends Collection
java.util.TreeSet<E>; // 一些构造函数
java.util.SortedSet<E>; // 获取比较器,最大最小元素
java.util.NavigableSet<E>; // 获取上下界,删除并返回等,递减顺序迭代器
注释: 从JavaSE 6 起, TreeSet 类实现了NavigableSet 接口。这个接口增加了几个便于定位元素以及反向遍历的方法。详细信息请参看API 注释。
队列和双端队列:
java.util.Queue<E> 是 interface
boolean add(E); // 增尾部
E remove(); // 删头部
E element(); // 返回头部(不删),以上异常抛出Exception
boolean offer(E); // 同add。满了返回false
E poll(); // 删头部。不存在返回null
E peek(); // 返回头部(不删)不存在返回null
java.util.Deque<E> 是 interface
// add remove get + (First or Last)
// offer poll peek + (First or Last)
java.util.ArrayDeque<E> 是 一种实现
ArrayDeque( ); // 16初始容量
ArrayDeque(int initialCapacity); // 给定初始容量
// 无限双端队列。
优先队列:
java.lang.Object
java.util.AbstractCollection<E>
java.util.AbstractQueue<E>
java.util.PriorityQueue<E>
priority queue
中元素可以按照任意顺序插入,却总按照排序的顺序进行检索。
堆数据结构存储,不必排序,无论何时调用remove总会获得当前优先队列中最小的元素。
add,remove会把最小元素移动到根,而不必排序。
迭代器不是按照顺序的!与TreeSet不同
9.3 映射
存放键值对。提供键查找值。
HashMap
和TreeMap
都实现了Map
接口。
Map<String, Employee> staff = new HashMap<>();// HashMap implements Map
Employee harry = new Employee("Harry Hacker");
staff.put("987-98-9996", harry); // 插入,第二个会取代第一个。返回旧值
String id = "987-98-9996";
e = staff.get(id);// gets harry 获得
int score = scores,get(id, 0); // 没有的话返回指定默认值0
staff.remove("567-24-2546") ; // 删除
scores.forEach((k, v) ->
System.out.println("key=" + k + ", value:" + v)) ; // 迭代用forEach
映射的包介绍
java.util.Map<K,V>
V get(Object key);
default V getOrDefault(Object key, V defaultValue);
V put(K key, V value);
void putAll(Map<? extends K,? extends V> m);
boolean containsKey(Object key);
boolean containsValue(Object value);
default void forEach(BiConsumer<? super K,? super V> action);
java.util.HashMap<K,V>:
java.lang.Object
java.util.AbstractMap<K,V>
java.util.HashMap<K,V>
java.util.TreeMap<K,V>:
java.lang.Object
java.util.AbstractMap<K,V>
java.util.TreeMap<K,V>
更新映射项:
三种合理的方式自增:
counts.put(word, counts.getOrDefault(word, 0)+ 1);
counts.putlfAbsent(word, 0);
counts.put(word, counts.get(word)+ 1);
counts.merge(word, 1, Integer::sum);
有一些方法,比如merge, computeIfPresent, computeIfAbsent, replaceAll
映射视图:
Map不是一个Collection,不过可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。
有三种视图:
键集合,值集合,键值对集合。
需要说明的是, keySet 不是HashSet 或TreeSet
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
可以用来枚举键、值、键值对。
for (String key : map.keySet()){} //遍历键值
for (Map.Entry<String, Employee> entry : staff.entrySet()){} //遍历键值对
counts.forEach((k, v) -> { // 最高效的遍历方式
do something with k, v
});
java.util.Map<K,V>:
Set<Map.Entry<K, V>> entrySet();
返回Map.Entry 对象(映射中的键/ 值对)的一个集视图。可以从这个集中删除元素,
它们将从映射中删除,但是不能增加任何元素。
Set<K> keySet();
返回映射中所有键的一个集视图。可以从这个集中删除元素,键和相关联的值将从映
射中删除,但是不能增加任何元素。
Collection<V> values();
返回映射中所有值的一个集合视图。可以从这个集合中删除元素, 所删除的值及相应
的键将从映射中删除,不过不能增加任何元素。
java.util.Map.Entry<K, V>
K getKey()
V getValue()
返回这一条目的键或值。
V setValue(V newValue)
将相关映射中的值改为新值, 并返回原来的值。
弱散列映射:
解决键不能被GC的问题。因为键在桶里,HashMap不删的话,就一直存在。
WeakHashMap
:当对键的唯一引用来自散列条目时, 这一数据结构将与垃圾回收器协同工作一起删除键/ 值对。
链接散列表与映射:
LinkedHashSet和LinkedHashMap会记录插入元素的顺序。
链接散列映射将用访问顺序, 而不是插入顺序, 对映射条目进行迭代。
每次调用get或put,受影响的条目将会被移动到链表尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。
访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。
枚举集与映射:
EmimSet 是一个枚举类型元素集的高效实现。位序列实现。bitmap?
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class); // allOf方法
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class); // noneOf方法
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY); // range方法
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY); // of 方法
EnumMap 是一个键类型为枚举类型的映射。值数组实现。构造器指定键类型。
EnumMap<Weekday, Employee〉personlnCharge = new EnumMapo(Weekday.class);
标识散列映射:
键的散列值不由hashCode计算,而是由System.identifyHashCode计算,根据内存地址计算散列码。用==比较,不用equals。
也就是说, 不同的键对象, 即使内容相同, 也被视为是不同的对象
9.4 视图与包装器
使用视图可以获得其他的实现了Collection接口和Map接口的对象(不一定是上图中的)。比如Map中的keySet方法所返回的Collection。这种可以对原类进行操作的集合称为视图。有很多有用的应用。
- 轻量级集合包装器
- 子范围
- 不可修改视图
- 同步视图
- 受查视图
- 轻量级集合包装器
Card[] cardDeck = new Card[52];
List<Card> cardList = Arrays.asList(cardDeck):
List<String> names = Arrays.asList("Amy", "Bob", "Carl") ;
List<String> settings = Collections.nCopies(100, "DEFAULT") ;
Collections.singleton(anObject)//返回实现Set接口,不可修改的单元素集。
- 子范围
List group2 = staff.subList(10, 20) ;
// 对于有序集和映射
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)
// 对于有序集和映射
SortedMap< K, V> subMap(K from, K to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)
// Java SE 6后
NavigableSet<E> subSet( E from , boolean fromlnclusive, E to, boolean tolnclusive)
NavigableSet<E> headSet(E to, boolean tolnclusive)
NavigableSet<E> tailSet(E from , boolean fromlnclusive)
- 不可修改的视图
这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改, 就抛出一个异常,同时这个集合将保持未修改的状态。
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap
List<String> staff = new LinkedList<>() ;
lookAt(Collections.unmodifiableList(staff)) ;
- 同步视图
类库的设计者使用视图机制来确保常规集合的线程安全, 而不是实现线程安全的集合类。
Map<String, Employee>map = Collections.synchronizedMap(new HashMap<String, Employee>());
- 受查视图
受査” 视图用来对泛型类型发生问题时提供调试支持。
一般情况下,add不报错,get时报错。用受查视图可在add时发现错误。
List<String> safestrings = Collections.checkedList(strings, String,class);
ArrayList rawList = safestrings;
rawList.add(new Date()); // checked list throws a ClassCastException
可选操作:
虽然视图是某种interface,但是不是所有操作都是可行的,有一些限制。