代码 = 数据结构 + 算法。JDK内部的集合框架就是方便我们处理数据的一系列工具。
集合框架的体系结构
Jdk的设计者在源码里只设计了两种体系,一个是类似数学意义上的集合,一个是类似数学上的映射。前者最高级别的抽象接口是Collection。后者最高级别的接口是Map。
Collection容器
如图所示,Collection是集合的顶级接口,定义了容器的基本行为特征。打开Collection接口,可以看到该接口有如下定义:
public interface Collection<E> extends Iterable<E> {
// 获得当前的容器容量
int size();
// 判定当前容器特征的一些方法
boolean isEmpty();
boolean contains(Object o);
//转换操作
Object[] toArray();
<T> T[] toArray(T[] a);
//操作集合内元素相关,并集 交集实现
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
// 迭代器
Iterator<E> iterator();
// 1.8新加默认实现方法,基于迭代器遍历实现
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
// 以下是流式编程的默认方法
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
// Object 自带方法
boolean equals(Object o);
int hashCode();
}
看起来方法很多,但是很好记忆:
- 针对集合自身,需要有描述自身情况的方法,例如是否非空,集合个数大小。
- 针对集合自身与元素的关系,需要定义一系列操作元素的方法,例如新增,删除,清理,遍历能力等。
- 针对集合与集合之间的关系,需要定义例如数学概念上的,并集,交集等等操作。
直接继承Collection接口实现的有三个,set,list,与queue。其中List是按照插入的顺序保存元素,Set中不能有重复的元素,而Queue按照排队规则来处理容器中的元素。
List子接口
List所代表的是有序的Collection,它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的索引来访问元素。List接口如下:
public interface List<E> extends Collection<E> {
// 和Collection接口定义重复的部分
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {return Spliterators.spliterator(this, Spliterator.ORDERED);}
// 这里开始和Collection不一样
// 提供按照位置获得数据,按照位置赋予数据,按照位置删除数据
// 按照数据进行匹配,返回其位置,并提供正向,反向匹配两种方向
E get(int index);
E set(int index, E element);
void add(int index, E element);
boolean addAll(int index, Collection<? extends E> c);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
// List专属的迭代器,扩展既有迭代器能力
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
// 获取列表的视图列表
List<E> subList(int fromIndex, int toIndex);
// 以下两个方法依赖ListIterator能力进行批量替换,排序
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
}
可以看到,List接口一方面吸收了Collection的接口方法定义,另外一方面加上了和位置相关的一些方法。这是符合我们的预判的,List是一个顺序表,其隐含着一种映射,就是自然顺位对应数据的一种映射。因此针对List容器的增删改查都需要和位置打上交道,这个逻辑才算是可以圆的过来。除此之外,List的接口还扩展了迭代器的能力,赋予ListIterator向前遍历的能力。再辅助几个default方法,利用上ListIterator的能力。
Set子接口
和数学上的集合概念类似,Set也不允许出现"同样"的值。Set接口定义如下:
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
和Collection一比较,你会发现,所有的方法都是Collection里面定义好了的。Collection全等于Set的定义。
Queue子接口
Queue字面含义是队列,打开这个类的接口定义:
public interface Queue<E> extends Collection<E> {
// 这部分和Collection重合
boolean add(E e);
// 下面这部分是Queue扩展而来
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
可以发现Queue在Collection接口上扩展出了更多操作元素的行为方法:
- offer方法:往这个队列里面加元素,和add干的事儿看上去一样,只不过换了一种更贴近队列操作的名称。在子类里面可能会出现实现不一样的场景,所以这里分开还是有必要的,指向的事儿不一样。
- remove方法:把队列头上的元素给删了
- poll方法:把队列头上的元素给删了,并获得到队列头的元素。相当于就是出队列,和remove干的事儿一样,只不过remove一个空的队列会抛出异常,poll会返回null,同样是因为语义不一致,所以造出来两个相近的方法。当子类实现的时候,针对这两个类似的方法,可能会出现实现逻辑差距越来越大的情况。
- element方法:瞅一眼队列头上的数据是啥,然后不删,队列里面继续还存在着。
- peek方法:类似于element()方法,区别是peek在操作空的队列的时候,不会报错,只是返回个null,但是Element会报错。
从继承关系上看,Queue还存在一个子接口,就是Deque接口,该接口在Queue的基础上增加了对头节点/尾节点的操作,类似offer方法,会出现offerFirst与offerLast方法。
Map容器
第二种容器类别就是Map了,本质上他是一种映射,在Map容器内部存储的是一个个K-V对。从上面的图可以看出来,Map接口是map族容器的顶级接口,他有很多实现。Map接口定义如下:
public interface Map<K,V> {
// 容器整体情况
int size();
boolean isEmpty();
// 键值对操作
boolean containsValue(Object value);
boolean containsKey(Object key);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
void clear();
// 键值对的转化
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
// 通用方法
boolean equals(Object o);
int hashCode();
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v);
try {
entry.setValue(v);
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
}
同样,方法很多,我们还是从基本概念着手进行记忆。Map是个容器,本质是个映射,映射依赖K-V键值对,Map容器承载的是大量K-V键值对。那么我们将Map的方法做以下的分类:
- 描述Map容器的整体情况,例如Map内有多少个键值对,是否是空的
- 表达Map与元素(键值对)间关系,例如往容器内存放,删除,获取键值对。依靠K 或者 V查找符合条件的键值对。
- 由于Map存储的数据不是极为简单的个体,因此Map的遍历是个麻烦的事情,这一点就需要要求Map具备将自身数据进行转化的能力。例如将K或者V全量抽取为集合,或者将K-V整体进行抽取为普通集合。
- 1.8引入大量default方法,这部分基本上都是在map基础上封装出来的易于操作的方法。
核心容器详解
List系容器
List接口继承自Collection,用于定义以列表形式存储的集合,List接口为集合中的每个对象标记了位置信息,依靠此信息来定位一个元素。List在Collection基础上增加的主要方法包括:
- get(int) - 返回指定index位置上的对象
- add(E)/add(int, E) - 在List末尾/指定index位置上插入一个对象
- set(int, E) - 替换置于List指定index位置上的对象
- indexOf(Object) - 返回指定对象在List中的index位置
- subList(int,int) - 返回指定起始index到终止index的子List对象
我们把所有的类的继承关系给生成出来,并去除掉部分不影响主逻辑抽象的接口:
从子类上看,我们只需要去了解:ArrayList,LinkedList,CopyOnWriteArrayList。虽说Vector和Stack已经被时代抛弃,但是了解他们有助于帮助我们扩充我们的知识面。其中CopyOnWriteArrayList 会涉及到一些并发知识,这部分将会在多线程模块详细描述。那么总结下来,我们需要花大力气了解ArrayList和LinkedList,而Vector和Stack稍微了解下即可。
ArrayList
我们把这个类的所有的继承关系全部拿出来,可以看到ArrayList继承自AbstractList,AbstractList又继承自AbstractCollection,符合我们的想象。从设计上说,ArrayList是一个线性容器,其内部使用一个数组作为存储。可以说ArrayList就是一个数组的封装,围绕着这个数组做复杂逻辑操作。
每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
由于ArrayList底层是数组,因此ArrayList擅长于随机访问。同时ArrayList是非同步的。
LinkedList
我们把LinkedList的继承图拉出来看看,你会发现,光看这个类的继承就比ArrayList复杂很多。当然LinkedList也是一个线性表,只不过背后的实现机制和ArrayList完全不一样。
LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。
Vector/Stack
Vector 与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组,它的操作与ArrayList几乎一样。Stack 继承自 Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。
Map系容器
Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。Java中提供的Map的实现主要有HashMap、LinkedHashMap、WeakHashMap、TreeMap、ConcurrentHashMap、ConcurrentSkipListMap,另外还有两个比较古老的Map实现HashTable和Properties。
Map接口在Collection的基础上,为其中的每个对象指定了一个key,并使用Entry保存每个key-value对,以实现通过key快速定位到对象(value)。Map接口的主要方法包括:
- size() - 集合内的对象数量
- put(K,V)/putAll(Map) - 向Map内添加单个/批量对象
- get(K) - 返回Key对应的对象
- remove(K) - 删除Key对应的对象
- keySet() - 返回包含Map中所有key的Set
- values() - 返回包含Map中所有value的Collection
- entrySet() - 返回包含Map中所有key-value对的EntrySet
- containsKey(K)/containsValue(V) - 判断Map中是否存在指定key/value
HashMap
HashMap 在内部定义了一个hash表数组(Entry[] table),Entry内存放键值对。在往HashMap内存放数据的时候,将会哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,如果冲突非常大则将链表数据转换为红黑树。
LinkedHashMap
LinkedHashMap继承自HashMap,在此基础之上增加了头尾节点,将存储到LinkedList的数据进行串联,产生链表结构。在HashMap中存在大量钩子,例如节点插入后,节点删除后,节点访问后的方法。在LinkedHashMap中覆盖创建普通节点,创建树节点以及覆盖前面说的节点相关操作方法以达到扩展行为的能力。基于这种扩展能力我们可以轻松创建出LRU性质的缓存Map。
TreeMap
TreeMap的继承关系如下,可以看到TreeMap实现NavigableMap接口,而NavigableMap接口则扩展了SortedMap。
SortedMap增强了Map接口,它定义了对键值对按照键的自然顺序或自定义顺序进行排序的功能。SortedMap中的键值对是按照键的顺序排列的,因此可以根据键的顺序进行范围查找和遍历操作。SortedMap接口提供了一系列的导航方法和有序操作方法。NavigableMap是SortedMap接口的子接口,它在SortedMap的基础上增加了一些额外的导航方法,使得对有序键值对的操作更加方便和灵活。NavigableMap接口提供了lowerKey、floorKey、ceilingKey、higherKey等导航方法,以及pollFirstEntry、pollLastEntry等移除并返回最小/最大键值对的方法。
TreeMap类:TreeMap是SortedMap接口的实现类,它使用红黑树数据结构来实现有序映射。TreeMap根据键的自然顺序或自定义比较器对键值对进行排序,并保持键值对的有序性。TreeMap提供了对键值对的插入、查找、删除和范围操作等常用功能。
WeakHashMap
WeakHashMap在实现上最大的不同之处在于Entry的构建,如下代码所示:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
...
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
...
}
可以看到Entry继承自,弱引用将会在GC的时候会被回收,导致Entry内的key消失并向queue中填充当前的Entry对象。从原理上来说,被清理的对象是key,但是从逻辑上来说,key被清理了整个Entry对象都应该是被废弃的。那什么时候会真正将Entry抛弃解除强引用呢?这个逻辑发生当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的key对象。同步它们,就是删除table中被GC回收的键值对。
IdentityhashMap
IdentityHashMap实现了Map接口,用法与HashMap差不多,都是用Hash表实现数据的存储,比较key的值是否相等,如果相等就替换原有的值。但是这个类和Hashmap最大的区别就是IdentityHash在比较key的时候使用的是地址,而普通的hashmap在比较key的时候使用的是equals。也正是由于IdentityHashmap的这个特点,那么在使用时需要小心。看一个例子:
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap();
String s1 = new String("test");
String s2 = new String("test");
identityHashMap.put(s1,"value");
identityHashMap.put(s2,"value1");
System.out.println(identityHashMap.get("test"));
System.out.println(identityHashMap);
获得到输出是:
null
{test=value1,test=value}
s1、s2 都是new出来的,所以他们的地址肯定不同,在使用get方法传递参数时”test“是直接放到了常量池中,所以地址和s1、s2也不同,所以get的结果是null。
EnumMap
EnumMap相对简单,首先是其key必须是个枚举类型,我们知道任何一个枚举其类型确定了之后,其内部使用orinary进行编号,对于map来说是一个天生的hash值,且不会冲突。因此,没有各种复杂的hash,链表,树结构。其底层就是很简单的数组类型,而枚举类型确定之后实际上可以感知到其内部的枚举量有多少,意味着在最开始就可以确定其长度,因此也不需要扩容缩容。
Set系容器
Set是一种不包括重复元素的Collection。Set容器大部分都是使用同名的Map来实现的,常用的有HashSet,LinkedHashSet,TreeSet,EnumSet。其他没什么好介绍的,区别都在细节上,详细介绍就参考JDK源码解析部分。
Queue系容器
Queue是一种叫做队列的数据结构,队列是遵循着一定原则的入队出队操作的集合,一般来说,入队是在队列尾添加元素,出队是在队列头删除元素,但是,也不一定,比如优先级队列的原则就稍微有些不同。队列主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedList。
流式编程
容器存在的意义是承载数据,然后就是处理数据。数据可以分为一维数据,二维数据,多维数据。用数组来表达就是一维数组,二维数组,N维数组。容器当然也是支持N维的。数据库使用的语言是SQL,是一种领域语言,我们称为DSL。SQL的出现是为了解决处理二维数据。如此对于我们的容器操作就犯了难了,当容器处理二维数据或者N维数据的时候,往往事情变的很麻烦,你需要写大量容器相关的代码才可以获得到你想要的结果。流式编程的出现,算是解放了生产力,它的写法在感官上和SQL语言的语义很类似。一些ORM框架甚至也通过类似的代码味道帮助生成你需要的SQL。而我们学习流式编程的前提需要了解Lamda表达式。
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();
() -> System.out.println("Hello Lambda Expressions");
show.addActionListener((e) -> {
System.out.println("Light, Camera, Action !! Lambda expressions Rocks");
});
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
features.forEach(n -> System.out.println(n));
// 使用Java 8的方法引用更方便,方法引用由::双冒号操作符标示,
// 看起来像C++的作用域解析运算符
features.forEach(System.out::println);
public static void main(args[]){
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
System.out.println("Languages which starts with J :");
filter(languages, (str)->str.startsWith("J"));
System.out.println("Languages which ends with a ");
filter(languages, (str)->str.endsWith("a"));
System.out.println("Print all languages :");
filter(languages, (str)->true);
System.out.println("Print no language : ");
filter(languages, (str)->false);
System.out.println("Print language whose length greater than 4:");
filter(languages, (str)->str.length() > 4);
}
public static void filter(List names, Predicate condition) {
for(String name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}
public static void filter(List names, Predicate condition) {
names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {
System.out.println(name + " ");
});
}
Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4;
names.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
costBeforeTax.stream().map((cost) -> cost + .12*cost).forEach(System.out::println);
// 新方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost) -> sum + cost).get();
System.out.println("Total : " + bill);
List<String> filtered = strList.stream().filter(x -> x.length()> 2).collect(Collectors.toList());
System.out.printf("Original List : %s, filtered list : %s %n", strList, filtered);
List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada");
String G7Countries = G7.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(", "));
System.out.println(G7Countries);
List<Integer> numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List<Integer> distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
System.out.printf("Original List : %s, Square Without duplicates : %s %n", numbers, distinct);
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : " + stats.getMax());
System.out.println("Lowest prime number in List : " + stats.getMin());
System.out.println("Sum of all prime numbers : " + stats.getSum());
System.out.println("Average of all prime numbers : " + stats.getAverage());
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); // 使用方法引用
list.forEach((String s) -> System.out.println("*" + s + "*"));
Lamda表达式语法
Lambda表达式是整个Java 8发行版中最受期待的在Java语言层面上的改变,也就是说这个东西就是个语法糖。Lambda允许把函数作为一个方法的参数进行传入,或者把代码看成数据,这也就是函数式编程。在JVM平台上的很多语言(Groovy,Scala,……)从一开始就有Lambda,但是Java这边方法必须在类里面承载,所以程序员不得不使用匿名类来传递方法,以达到函数式编程的味道。关于Lambda设计的讨论占用了大量的时间与社区的努力。值得高兴的是,最终找到了一个平衡点,使得可以使用一种即简洁又紧凑的新方式来构造Lambdas。其语法形式为 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符。
Comparable<Integer> comparable = new Comparable<Integer>() {
@Override
public int compareTo(Integer o) {
return 0;
}
};
可以使用lamda写成
Comparable<Integer> comparable = o -> 0;
是不是很神奇,很多代码直接就没了。搞的丰满一点:
Comparable<Integer> comparable = (o) -> { return 0;};
这样看是不是和匿名类的写法可以对应上?从上面的例子可以看出来,我们的lamda表达式事实上是依赖于接口定义的,如果想写一个lamda表达式,需要要求接口中只能有一个需要被实现的方法,这里注意,java8里面可以写上default方法,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。除此之外,还需要使用@FunctionalInterface
来标注接口,但是也不是必须,只要符合前面说的,一个接口内只有一个等待实现的方法即可。我们的接口方法可以分为无参,有一个参数,多参,返回可以有两种可能性,一个是有返回,一个是没有返回。因此这样的组合有6种,我们都把他们列出来,后面我们的例子就只使用这6种接口来玩儿。
/**多参数无返回*/
@FunctionalInterface
public interface NoReturnMultiParam {
void method(int a, int b);
}
/**无参无返回值*/
@FunctionalInterface
public interface NoReturnNoParam {
void method();
}
/**一个参数无返回*/
@FunctionalInterface
public interface NoReturnOneParam {
void method(int a);
}
/**多个参数有返回值*/
@FunctionalInterface
public interface ReturnMultiParam {
int method(int a, int b);
}
/*** 无参有返回*/
@FunctionalInterface
public interface ReturnNoParam {
int method();
}
/**一个参数有返回值*/
@FunctionalInterface
public interface ReturnOneParam {
int method(int a);
}
如此一来,我们来写个测试case,把所有的可能性都放进去
public class Test1 {
public static void main(String[] args) {
//无参无返回
NoReturnNoParam noReturnNoParam = () -> {
System.out.println("NoReturnNoParam");
};
noReturnNoParam.method();
//一个参数无返回
NoReturnOneParam noReturnOneParam = (int a) -> {
System.out.println("NoReturnOneParam param:" + a);
};
noReturnOneParam.method(6);
//多个参数无返回
NoReturnMultiParam noReturnMultiParam = (int a, int b) -> {
System.out.println("NoReturnMultiParam param:" + "{" + a +"," + + b +"}");
};
noReturnMultiParam.method(6, 8);
//无参有返回值
ReturnNoParam returnNoParam = () -> {
System.out.print("ReturnNoParam");
return 1;
};
int res = returnNoParam.method();
System.out.println("return:" + res);
//一个参数有返回值
ReturnOneParam returnOneParam = (int a) -> {
System.out.println("ReturnOneParam param:" + a);
return 1;
};
int res2 = returnOneParam.method(6);
System.out.println("return:" + res2);
//多个参数有返回值
ReturnMultiParam returnMultiParam = (int a, int b) -> {
System.out.println("ReturnMultiParam param:" + "{" + a + "," + b +"}");
return 1;
};
int res3 = returnMultiParam.method(6, 8);
System.out.println("return:" + res3);
}
}
我们可以通过观察以下代码来完成代码的进一步简化,写出更加优雅的代码。
public class Test2 {
public static void main(String[] args) {
//1.简化参数类型,可以不写参数类型,但是必须所有参数都不写
NoReturnMultiParam lamdba1 = (a, b) -> {
System.out.println("简化参数类型");
};
lamdba1.method(1, 2);
//2.简化参数小括号,如果只有一个参数则可以省略参数小括号
NoReturnOneParam lambda2 = a -> {
System.out.println("简化参数小括号");
};
lambda2.method(1);
//3.简化方法体大括号,如果方法条只有一条语句,则可以胜率方法体大括号
NoReturnNoParam lambda3 = () -> System.out.println("简化方法体大括号");
lambda3.method();
//4.如果方法体只有一条语句,并且是 return 语句,则可以省略方法体大括号
ReturnOneParam lambda4 = a -> a+3;
System.out.println(lambda4.method(5));
ReturnMultiParam lambda5 = (a, b) -> a+b;
System.out.println(lambda5.method(1, 1));
}
}
有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以可以利用 lambda表达式的接口快速指向一个已经被实现的方法。其语法是:方法归属者::方法名 静态方法的归属者为类名,普通方法归属者为对象
public class Exe1 {
public static void main(String[] args) {
ReturnOneParam lambda1 = a -> doubleNum(a);
System.out.println(lambda1.method(3));
//lambda2 引用了已经实现的 doubleNum 方法
ReturnOneParam lambda2 = Exe1::doubleNum;
System.out.println(lambda2.method(3));
Exe1 exe = new Exe1();
//lambda4 引用了已经实现的 addTwo 方法
ReturnOneParam lambda4 = exe::addTwo;
System.out.println(lambda4.method(2));
}
/**
* 要求
* 1.参数数量和类型要与接口中定义的一致
* 2.返回值类型要与接口中定义的一致
*/
public static int doubleNum(int a) {
return a * 2;
}
public int addTwo(int a) {
return a + 2;
}
}
一般我们需要声明接口,该接口作为对象的生成器,通过 类名::new 的方式来实例化对象,然后调用方法返回对象。
interface ItemCreatorBlankConstruct {
Item getItem();
}
interface ItemCreatorParamContruct {
Item getItem(int id, String name, double price);
}
public class Exe2 {
public static void main(String[] args) {
ItemCreatorBlankConstruct creator = () -> new Item();
Item item = creator.getItem();
ItemCreatorBlankConstruct creator2 = Item::new;
Item item2 = creator2.getItem();
ItemCreatorParamContruct creator3 = Item::new;
Item item3 = creator3.getItem(112, "鼠标", 135.99);
}
}
常用函数式接口
java内自己携带了很多拿来即用的函数式接口
- Predicate boolean test(T t) 传入一个参数返回boolean值
- Consumer void accept(T t) 传入一个参数,无返回值
- Function<T,R> R apply(T t) 传入一个参数,返回另一个类型
上面列举的是在java.util.function�包内具有代表性的接口,除此之外还存在大量其他函数式接口,在使用的时候可以翻一翻。
Lambda表达式常用示例
创建线程
我们以往都是通过创建 Thread 对象,然后通过匿名内部类重写 run() 方法,一提到匿名内部类我们就应该想到可以使用 lambda 表达式来简化线程的创建过程。
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(2 + ":" + i);
}
});
t.start();
遍历集合
我们可以调用集合的 public void forEach(Consumer<? super E> action)
方法,通过 lambda 表达式的方式遍历集合中的元素。以下是 Consumer 接口的方法以及遍历集合的操作。Consumer 接口是 jdk 为我们提供的一个函数式接口。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
//....
}
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1,2,3,4,5);
//lambda表达式 方法引用
list.forEach(System.out::println);
list.forEach(element -> {
if (element % 2 == 0) {
System.out.println(element);
}
});
删除集合中的某个元素
我们通过public boolean removeIf(Predicate<? super E> filter)
方法来删除集合中的某个元素,Predicate 也是 jdk 为我们提供的一个函数式接口,可以简化程序的编写。
ArrayList<Item> items = new ArrayList<>();
items.add(new Item(11, "小牙刷", 12.05 ));
items.add(new Item(5, "日本马桶盖", 999.05 ));
items.add(new Item(7, "格力空调", 888.88 ));
items.add(new Item(17, "肥皂", 2.00 ));
items.add(new Item(9, "冰箱", 4200.00 ));
items.removeIf(ele -> ele.getId() == 7);
//通过 foreach 遍历,查看是否已经删除
items.forEach(System.out::println);
集合内元素的排序
在以前我们若要为集合内的元素排序,就必须调用 sort 方法,传入比较器匿名内部类重写 compare 方法,我们现在可以使用 lambda 表达式来简化代码。
ArrayList<Item> list = new ArrayList<>();
list.add(new Item(13, "背心", 7.80));
list.add(new Item(11, "半袖", 37.80));
list.add(new Item(14, "风衣", 139.80));
list.add(new Item(12, "秋裤", 55.33));
/*
list.sort(new Comparator<Item>() {
@Override
public int compare(Item o1, Item o2) {
return o1.getId() - o2.getId();
}
});
*/
list.sort((o1, o2) -> o1.getId() - o2.getId());
System.out.println(list);
Lambda闭包问题
这个问题我们在匿名内部类中也会存在,如果我们把注释放开会报错,告诉我 num 值是 final 不能被改变。这里我们虽然没有标识 num 类型为 final,但是在编译期间虚拟机会帮我们加上 final 修饰关键字。
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
int num = 10;
Consumer<String> consumer = ele -> {
System.out.println(num);
};
//num = num + 2;
consumer.accept("hello");
}
}
Stream流操作
有了lamda表达式之后,我们针对集合的操作就可以非常简单,纯粹看api调用会发现其操作与Sql书写很相似
Stream流创建
一般来说,希望使用lamda表达式来操作集合的场合下,都需要从集合产生Stream对象。我们知道Collection接口在集合体系内是最顶级接口,在1.8内已经默认存在了两个产生Stream的方法:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
这两个方法分别是普通的流与并行流。其中并行流将会使用多线程的方式执行对集合的操作。除了直接调用集合产生Stream流,还要一下方法可以帮助我们创建Stream
//1.集合
Stream<Student> stream = list.stream();
//2.静态方法
Stream<String> stream2 = Stream.of("a", "b", "c");
//3.数组
String[] arr = {"a","b","c"};
Stream<String> stream3 = Arrays.stream(arr);
Stream流使用
先举一个简单的例子用作说明:
List<String> strs = Arrays.asList("1","2","2","3");
List<String> out = strs.stream()
.filter(e->"2".equals(e))
.collect(Collectors.toList());
上面的代码很简单,就是对数据进行过滤,保留不是2的元素,最终得到,“1”,“3”的集合。举这个例子的目的是为了说明终止操作与非终止操作。Stream调用api并非立刻执行,除非碰到了终止操作,区分是否非终止操作也很简单,看执行api之后是不是返回Stream流即可。上面代码中filter调用之后返回的是Stream,也就是非终止操作。而执行collect的时候将返回一个列表数据。那就是终止操作了。
Stream流的非终止操作有这些:
- filter(Predicate) 筛选流中某些元素
- map(Function f) 接收流中元素,并且将其映射成为新元素
- flatMap(Function f) 将所有流中的元素并到一起连接成一个流
- peek(Consumer c) 获取流中元素,操作流中元素,与foreach不同的是不会截断流,可继续操作流
- distinct() 通过流所生成元素的equals和hashCode去重
- limit(long val) 截断流,取流中前val个元素
- sorted(Comparator) 产生一个新流,按照比较器规则排序
- sorted() 产生一个新流,按照自然顺序排序
Stream流的终止操作有这些:
- foreach(Consumer c) 遍历操作
- collect(Collector) 将流转化为其他形式
- max(Comparator) 返回流中最大值
- min(Comparator) 返回流中最小值
- count 返回流中元素总数
- booelan allMatch(Predicate) 都符合
- boolean anyMatch(Predicate) 任一元素符合
- boolean noneMatch(Predicate) 都不符合
- findFirst 返回第一个元素
- findAny 返回当前流中的任意元素
接下来我们对上面出现的api都做下demo,具体看看怎么用的,先看非终止操作
@Test
public void test001(){
List<List<Integer>> list1 = Arrays.asList(
Arrays.asList(3,1,5,9,101),
Arrays.asList(13,15,11,99,99)
);
List<Integer> out = list1.stream()
.flatMap(e->e.stream()) // 将列表数据内的列表打平为元素
.filter(e->e < 100) // 过滤,将流内100的值剔除
.peek(e->System.out.println("遍历数据:"+e)) // peek可以遍历数据而不截断
.distinct() // 将两个99 变为一个99
.sorted() // 进行排序
.limit(5) // 仅选取5个
.map(e->e +1) // 遍历映射,可以对数据进行加工产生新的流
.collect(Collectors.toList()); // 从流产生列表数据
System.out.println(out);
}
获得到的输出是:
遍历数据:3
遍历数据:1
遍历数据:5
遍历数据:9
遍历数据:13
遍历数据:15
遍历数据:11
遍历数据:99
遍历数据:99
[2, 4, 6, 10, 12]
再看看终止操作:
@Test
public void test002(){
List<Integer> list = Arrays.asList(1,2,3,4,5);
System.out.println("Min:"+ list.stream().min(Integer::compareTo));
System.out.println("Max:"+ list.stream().max(Integer::compareTo));
System.out.println("count:"+ list.stream().count());
System.out.println("allMatch: <=5 :"+list.stream().allMatch(e->e <=5));
System.out.println("allMatch: <=4 :"+list.stream().allMatch(e->e <=4));
System.out.println("anyMatch: >=5 :"+list.stream().anyMatch(e->e >=5));
System.out.println("anyMatch: >=6 :"+list.stream().anyMatch(e->e >=6));
System.out.println("noneMatch: ==5 :"+list.stream().noneMatch(e->e ==5));
System.out.println("noneMatch: ==6 :"+list.stream().noneMatch(e->e ==6));
System.out.println("findFirst: ==6 :"+list.stream().findFirst());
System.out.println("findAny: ==6 :"+list.stream().findAny());
}
得到输出是:
Min:Optional[1]
Max:Optional[5]
count:5
allMatch: <=5 :true
allMatch: <=4 :false
anyMatch: >=5 :true
anyMatch: >=6 :false
noneMatch: ==5 :false
noneMatch: ==6 :true
findFirst: ==6 :Optional[1]
findAny: ==6 :Optional[1]
上述api中最特殊的是collect方法,其传参来自Collectors类,表达如何收集流中数据。这里列举下常用的一些收集方法:
// class Student{
// 学号,班级号,名字
// }
List<Student> students = Arrays.asList(
new Student("001","class1", "张三"),
new Student("002","class1", "李四"),
new Student("003","class2", "王五"),
)
List<String> list1 = Arrays.asList("1","2","3");
// 1) 收集为list数据
List<String> out1 = list1.stream().collect(Collectors.toList());
// 2) 收集为set数据
Set<String> out2 = list1.stream().collect(Collectors.toSet());
// 3) 操作对象列表成为 名字 -> 对象的映射
Map<String,Student> nameMap = students.stream().collect(Collectors.toMap(e->e.getName(), e->e));
// 4)操作对象列表分组 班级 -> 对象列表
Map<String, List<Student>> classMap = students.stream().collect(Collectors.groupingBy(e->e.getClassName()));
Collections集合工具类
Collections是一个jdk自带的集合工具类,包含大量工具方法。详细及其源码请翻阅jdk源码分析专栏。这里仅做简单介绍。
一个大类别是封装操作,例如将不具备线程安全的容器封装为安全性容器,其本质是为每个方法都套上一层sycnized关键字。这里罗列一下都有哪些:
- 将容器封装为不可变的集合对象,也可以制造n个相同元素的不可变集合(逻辑上是n个不可变集合,但是底层仅存储一个元素)
- 将容器封装为具备检查性的集合对象,以checked打头的一系列方法,实际上这个是泛型出现之前的类型安全容器的解决方案。
- 将元素封装为单元素容器集合对象
- 产生空容器集合对象(单例),以Empty打头的一系列static一系列方法,准确的说不算封装,而是一个容器产生器。
第二个大类就是针对容器元素进行操作的方法,这部分繁杂且多
- 感知容器的信息,例如可以得到容器内最大最小值,统计某个元素出现的频次
- 查询容器的元素,Collections内有二分查找能力
- 操作容器的元素,我们大致分为以下三组:
- 针对容器对象本身:使用一个对象进行填充容器
- 针对容器元素顺序:排序,反转列表顺序,随机乱排顺序,回环移动位置
- 针对容器元素本身:可以交换两个元素的位置,可以替换元素值