重学Java - 进阶(2/2)

关键字:集合Collection 接口Map 接口泛型Java IO反射Java 8 新特性Stream APIOptional

引言

【因内容较多,已拆分为两篇博文,此为篇二】
本篇博文为 Java 的一些高级特性的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 基础。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。

一、集合

集合、数组都是对多个数据进行存储(内存层面)操作的结构,即容器

数组在内存存储方面的特点:

  • 数组初始化后长度确定
  • 数组声明的类型就决定了进行元素初始化时的类型

数组在存储数据方面的弊端:

  • 数组初始化后长度不可变,不便于拓展
  • 数组中提供的属性和方法不便于进行添加、删除、插入等操作,且效率不高
    无法直接获取已存储元素的个数
  • 数组存储的数据是有序(存入顺序)的、可重复的 —— 存储数据的特点单一

集合类可用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组

Java 集合可分为 Collection 和 Map 两种体系:

  • Collection 接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合
      • ArrayList、LinkedList、Vector
    • Set:元素无序、不可重复的集合
      • HashSet、LinkedHashSet、TreeSet
  • Map 接口:双列数据,保存具有映射关系 "key-value" 对 的集合
    • HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

二、Collection 接口

常用方法:(例:Collection collectionVar = new ArrayList(); )

  • add(Object e):将元素 e 添加到集合(collectionVar)中
  • size():获取集合(collectionVar)中的元素个数
  • addAll(Collection collection):将集合(collection)中的元素添加到当前集合(collectionVar)中
  • isEmpty():判断当前集合(collectionVar)是否为空
  • clear():清空集合(collectionVar)中的元素
  • contains(Object obj):判断当前集合(collectionVar)中是否包含 obj
    • 向 Collection 接口实现类的对象中添加数据 obj 时,要求 obj 所在类要重写 equals() 方法
    • 判断时会调用 obj 对象所在类的 equals() 方法
  • containsAll(Collection collection):判断集合(collection)中的所有元素是否都存在于当前集合(collectionVar)中
  • remove(Object obj):从当前集合(collectionVar)中移除 obj 元素
  • removeAll(Collection collection):从当前集合(collectionVar)中移除指定集合(collection)中所有的元素(求差集)
  • retainAll(Colllection collection):保留当前集合(collectionVar)与指定集合(collection)中所有相同的元素(求交集)
  • equals(Object obj):判断当前集合(collectionVar)与指定 obj 中元素是否相同
    • 针对有序集合与元素顺序也相关
    • 针对无序集合元素顺序无关
  • hashCode():返回当前对象的哈希值
  • 集合 -> 数组:toArray()
    • 数组 -> 集合:调用 Arrays 类的静态方法 asList()
      • 注意:Arrays.asList(new int[]{1, 2, 3}) 与 Arrays.asList(new Integer[]{1, 2, 3}) 不同,前者结果为包含 1 个元素(int 数组对象)的集合

2.1、Iterator 迭代器接口

Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素

  • GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需要暴露该对象的内部细节。迭代器模式就是为容器而生
  • Iterator 仅用于遍历集合,Iterator 本身并不提供承载对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合
  • 集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
  • Iterator 内部定义了 remove() 方法,可以在遍历的时候删除集合中的元素
    • 此方法不同于集合直接调用 remove()
    • 如果还未调用 next() 或在上一次调用 next() 之后已经调用了 remove(),再次调用 remove() 将会抛出异常:IllegalStateException
  • 常用方法:
    • hasNext():判断是否还有下一个元素
    • next():指针下移并将下移后集合位置上的元素返回
    • 建议在调用 next() 方法之间必须要调用 hasNext() 进行检测。若不进行检测,且下一条记录无效,直接调用 next() 会抛出异常:NoSuchElementException

2.2、foreach 循环遍历

Java5.0 提供了 foreach 循环迭代访问 Collection 和数组

  • 遍历操作不需要获取 Collection 或数组的长度,无需使用索引访问元素
  • 遍历结合的底层调用 Iterator 完成操作
  • 使用方法:
    • for(集合元素的类型 局部变量 : 集合对象){}
    • for(数组元素的类型 局部变量 : 数组对象){}

2.3、List 接口

List 接口的常用实现类:ArrayList、LinkedList 和 Vector

ArrayList、LinkedList 和 Vector 的对比:

  • 相同点:三个类都实现了 List 接口,存储数据的特点相同:存储有序的、可重复的数据

  • 不同点:

    • ArrayList:线程不安全,效率高;底层使用 Object[] elementData 存储
    • LinkedList:对于频繁的插入、删除操作使用此类效率比 ArrayList 高:底层使用双线链表存储
    • Vector:线程安全,效率低;底层使用 Object[] elementData 存储
    • Vector 在扩容方面默认扩容为原来数组长度的 2 倍

2.3.1、ArrayList

ArrayList 源码分析(JDK7):

  • ArrayList list = new ArrayList();
    • 底层创建长度为 10 的 Object[] 数组 elementData
  • list.add(1);
    • elementData[0] = new Integer(1);
  • list.add(2);list.add(3); 以此类推 list.add(11);
    • list.add(11); 导致底层 elementData 数组容量不足,则扩容
    • 默认情况下,扩容为原容量 1.5 倍,同时将原数组中元素复制到新数组中
  • 注:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity);

ArrayList 源码分析(JDK8):

  • ArrayList list = new ArrayList();
    • 底层 Object[] elementData 初始化为 {},并没有创建长度为 10 的数组
  • list.add(1);
    • 第一次调用 add() 时,底层才创建了长度为 10 的数组,并将数据添加到 elementData[0]
  • 后续添加与扩容操作与 JDK7 一致

注:JDK7 中 ArrayList 的对象创建类似于单例模式中的饿汉式;JDK8 中 ArrayList 的对象创建类似于单例模式的懒汉式,延迟了数组的创建,节省内存

2.3.2、LinkedList

LinkedList:双向链表,内部没有声明数组,而是定义了 Node 类型的 first 和 last 用于记录首、尾元素

  • 内部类 Node 作为 LinkedList 中保存数据的基本结构
  • Node 除保存数据,还定义了两个变量:
    • prev 变量记录前一个元素的位置
    • next 变量记录后一个元素的位置
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
    
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 源码分析:

  • LinkedList list = new LinkedList();
    • 内部声明了 Node 类型的 first 和 last 属性,默认值为 null
  • list.add(1);
    • 创建 Node 对象,将数据封装到 Node 中

2.3.3、List 接口方法

List 除了从 Collection 继承的方法外还增加了一些根据索引来操作集合元素的方法

  • void add(int index, Object obj):在 index 位置插入 obj 元素
  • boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
  • Object get(int index):获取指定 index 位置的元素
  • int indexOf(Object obj):返回 obj 在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回 obj 在集合中最后一次出现的位置
  • Object remove(int index):移除指定 index 位置的元素,并返回此元素
    • 注意区分 remove(int index) 和 remove(Object obj)
  • Object set(int index, Object obj):设置指定 index 位置的元素 为 obj
  • List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合

2.4、Set 接口

Set 接口的常用实现类:HashSet、LinkedSet、TreeSet

HashSet、LinkedSet 和 TreeSet的对比:

  • 相同点:三个类都实现了 Set 接口,存储数据的特点相同:存储无序的、不可重复的数据

  • 不同点:

    • HashSet:线程不安全;可以存储 null
    • LinkedSet:作为 HashSet 的子类:遍历其内部数据时,可以按照添加顺序遍历
      • 对于频繁的遍历操作,LinkedHashSet 效率高于 HashSet
    • TreeSet:可以按照添加对象的指定属性进行排序

Set 接口中没有额外定义新的方法,使用的都是 Collection 中声明过的方法

无序性:不等于随机性

  • 以 HashSet 为例:存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据 hash 值决定的

不可重复性:相同的元素只能添加一个(保证添加的元素按照 equal() 方法判断时返回结果为 false)

2.4.1、HashSet

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用该实现类

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有较好的存取、查找、删除性能

HashSet 底层结构:数组 + 链表

  • 数组初始容量为 16,当使用率超过其 0.75 倍,则扩容为原来的 2 倍

HashSet 的特点:

  • 不能保证元素的排列顺序
  • HashSet 不是线程安全的
  • 集合元素可以是 null

对于存放在 Set 容器中的对象,对应的类必须重写 equals() 方法和 hashCode(Object obj) 方法,以实现对象的相等判断规则(相等的对象必须具有相等的散列码)

  • 重写的原则:
    • 通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算
    • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值
    • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等
    • 对象中用作 equals() 方法比较的 Field 都应该用来计算 hashCode 值

Eclipse 与 IntelliJ IDEA 工具中 hashCode() 的重写,可以调用工具自动完成,这里有个数字 31

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的 hash 地址越大,所谓的 "冲突" 就越少,查找起来效率也会提高(减少冲突)
  • 并且 31 只占用 5bits,相乘造成的数据溢出的概率较小
  • 31 可以由 i*31==(i<<5)-1 来表示(提高算法效率)
  • 31 是一个素数,素数作用就是如果用一个数字乘这个素数,那么结果只能被这个素数本身和被乘数以及 1 整除(减少冲突)

向 HashSet 中添加元素的过程:

  • 向 HashSet 中添加新元素,首先调用新元素所在类的 hashCode() 方法,计算新元素的 hash 值,此 hash 值接着通过某种散列函数计算出在 HashSet 底层数组中的存储位置(索引位置),接着判断数组此位置上是否已经有元素:
    • 若此位置没有其他元素,则新元素添加成功(情况 A)
    • 若此位置已有元素(或以链表形式存在的多个元素),则比较新元素与已有元素的 hash 值:
      • 若 hash 值不相同,则新元素添加成功(情况 B)
      • 若 hash 值相同,进而需要调用新元素所在类的 equals() 方法:
        • equals() 返回 false,则新元素添加成功(情况 C)
        • equals() 返回 true,则新元素添加失败

对于新元素添加成功的情况而言:B 与 C 的情况下,新元素与已经存在的指定索引位置上的数据以链表的方式存储

  • JDK7:新元素存放在数组中,指向原来的元素
  • JDK8:原来的元素在数组中,指向新元素

向 HashSet 中添加元素的过程:

  • 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计得越好)
  • 如果两个元素的 hashCode 值相等,会再继续调用 equals() 方法,如果 equals() 方法结果为 true,添加失败;如果为 false,那么会保存该元素,但是该数组的位置已经有元素,因此会通过链表的方式继续链接
  • 如果两个元素的 equals() 方法返回 true,但它们的 hashCode 值不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功

2.4.2、LinkedHashSet

LinkedHashSet 是 HashSet 的子类

  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
    • 在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据
  • LinkedHashSet 的插入性能略低于 HashSet,但在迭代访问 Set 集合中全部元素时有很好的性能

2.4.3、TreeSet

TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态

  • TreeSet 底层使用红黑树结构存储数据
  • TreeSet 两种排序方法
    • (默认采用)自然排序 Comparable
      • 比较两个对象是否相同的标准为:compareTo() 返回 0。(不再是 equals())
    • (传入参数)定制排序 Comparator
      • 比较两个对象是否相同的标准为:compare() 返回 0。(不再是 equals())
  • 向 TreeSet 中添加的数据要求是相同类的对象

三、Map 接口

Map:双列数据,存储 key-value对 数据

  • HashMap:线程不安全,效率高;key 和 value 可以为 null
    • LinkedHashMap:保证在遍历时可以按照添加的顺序
      在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素
      对于频繁的遍历操作,此类执行效率高于 HashMap
  • TreeMap:保证按照添加的 key-value对 进行排序,实现排序遍历。(考虑 key 的自然排序或定制排序)
  • Hashtable:线程安全,效率低;key 和 value 不能为 null
    • Properties:常用来处理配置文件。key 和 value 都是 String 类型

3.1、Map 结构

  • Map 中的 key:无序、不可重复,使用 Set 存储(HashMap:key 所在的类要重写 equals() 方法和 hashCode() 方法)
  • Map 中的 value:无序、可重复,使用 Collection 存储(value 所在的类要重写 equals() 方法)
  • 一个键值对:key-value 构成一个 Entry 对象
  • Map 中的 Entry 的对象(entry):无序、不可重复,使用 Set 存储

3.2、Map 中的常用方法

  • 添加、删除、修改操作:
    • Object put(Object key, Object value):将指定 key-value 添加到(或修改 value)当前 map 对象中
    • void putAll(Map m):将 m 中的所有 key-value对 存放到当前 map 中
    • Object remove(Object key):移除指定 key 的 key-value对,并返回 value
    • void clear():清空当前 map 中的所有数据
  • 查询操作:
    • Objec get(Object key):获取指定 key 对应的 value
    • boolean containsKey(Object key):判断是否包含指定的 key
    • boolean containsValue(Object value):判断是否包含指定的 value
    • int size():返回 map 中 key-value对 的个数
    • boolean isEmpty():判断当前 map 是否为空
    • boolean equals(Object obj):判断当前 map 和 obj 是否相等
  • 元视图操作:
    • Set keySet():返回所有 key 构成的 Set 集合
    • Collection values():返回所有 value 构成的 Collection 集合
    • Set entrySet():返回所有 key-value对 构成的 Set 集合

3.2.1、HashMap

HashMap 的底层实现原理(JDK7):

  • HashMap map = new HashMap(); :底层创建长度为 16 的一维数组 Entry[] table
  • map.put(key, value); :首先调用 key 所在类的 hashCode() 方法计算 key 的 hash 值,此 hash 值经过某种算法计算得到 Entry 数组中的存放位置
    • 若此位置数据为空,此时 key-value 添加成功(情况 A)
    • 若此位置数据不为空(存在一个或多个数据(以链表形式存在)),比较 key 与已存在的一个或多个数据的 hash 值
      • 若 key 的 hash 值与已存在的数据的 hash 值都不相同,此时 key-value 添加成功(情况 B)
      • 若 key 的 hash 值与已存在的某一个数据的 hash 值相同,则调用 key 所在类的 equals() 与这个原数据的 key 做比较
        • 若 equals() 返回 false,此时 key-value 添加成功(情况 C)
        • 若 equals() 返回 true,则使用 value 替换原数据的 value

对于 key-value 添加成功的情况而言:B 与 C 的情况下,key-value 与原数据以链表的方式存储

在添加过程中涉及到扩容问题,当超出临界值时(且要存放的位置非空)默认扩容为 2 倍

HashMap 在 JDK8 与 JDK7 底层实现方面的区别(针对 JDK8 而言):

  • new HashMap():底层没有直接创建一个长度为 16 的数组
  • JDK8 的底层数组是 Node[],不再是 Entry[]
  • 首次调用 put() 方法时,底层创建长度为 16 的数组
  • JDK7 底层实现结构:数组 + 链表;JDK8 底层实现结构:数组 + 链表 + 红黑树
    • 当数组的某一索引位置上的元素以链表形式存在的数据个数大于 8 且当前数组的长度大于 64 则此时索引位置上的所有数据改为使用红黑树存储

HashMap 源码中的主要常量:

  • DEFAULT_INITAL_CAPACITY:HashMap 的默认容量
  • MAXIMUM_CAPACITY:HashMap 的最大支持容量 2^30
  • DEFAULT_LOAD_FACTOR:HashMap 的默认负载因子
  • TREEIFY_THRESHOLD:Bucket 中链表长度大于该默认值,转化为红黑树
  • UNTREEIFY_THRESHOLD:Bucket 中红黑树存储的 Node 小于该默认值,转化为链表
  • MIN_TREEIFY_CAPACITY:Bucket 中的 Node 被树化时最小的 hash 表容量
    当 Bucket 中 Node 的数量大到需要转红黑树时,若 hash 表容量小于 MIN_TREEIFY_CAPACITY,此时应执行 resize 扩容这个 MIN_TREEIFY_CAPACITY 的值至少是 TREEIFY_THRESHOLD 的 4 倍
  • table:存储元素的数组,总是 2 的 n 次幂
  • entrySet:存储具体元素的集合
  • size:HashMap 中存储的键值对的数量
  • modeCount:HashMap 扩容和结构改变的次数
  • threshold:扩容的临界值(容量 * 负载因子)
  • loadFactor:负载因子

负载因子值的大小,对 HashMap 的影响:

  • 负载因子的大小决定了 HashMap 的数据密度
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或添加时的比较次数增多,性能会下降
  • 负载因子越小,就越容易触发扩容,数据密度也越小,即发生碰撞的几率越小,数组中的链表也就越短,查询和添加时比较的次数也越少,性能会更高。但是会浪费一定的内存空间,而且经常扩容也会影响性能,因此建议初始化预设大一点的空间
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为 0.7 ~ 0.75,此时平均检索长度接近于常数

3.2.2、LinkedHashMap

HashMap 中的内部类:Node

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

LinkedHashMap 中的内部类:Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;  // 实现记录添加的元素的先后顺序
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

3.2.3、Properties

Properties 类是 Hashtable 的子类,该对象用于处理配置文件

  • 配置文件中的 key、value 都是字符串类型,所以 Properties 中的 key、value 都是字符串类型
  • 存取数据时,建议使用 setProperty(String key, String value) 方法和 getProperty(string key) 方法
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String db = pros.getProperty("db");

3.3、Collections 工具类

Collections 是一个操作 List、Set 和 Map 等集合的工具类

  • Collections 中提供了一系列静态方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

3.3.1、Collections 常用方法:同步控制

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可以将指定集合包装为线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

  • synchronizedCollection(Collection<T> c)
  • synchronizedList(List<T> list)
  • synchronizedMap(Map<K, T> m)
  • synchronizedSet(Set<T>, s)
  • synchronizedSortedMap(SortedMap<K, V>, m)
  • synchronizedSortedSet(Sorted<T> s)

3.3.2、Collections 常用方法

排序操作(static 方法):

  • reverse(List list):反转 list 中元素的顺序
  • shuffle(List list):对 list 集合元素进行随机排序
  • sort(List list):对 list 集合元素进行自然排序(升序)
  • sort(List list, Comparator comparator):根据指定的 comparator 对 list 集合元素进行定制排序
  • swap(List list, int i, int j):将指定 list 集合中 i 处元素和 j 处元素进行交换

查找、替换:

  • Object max(Collection collection):根据元素自然排序结果,返回给定集合中的最大元素
  • Object max(Collection collection, Comparator comparator):根据 comparator 指定的排序规则,返回给定集合中的最大元素
  • Object min(Collection collection):根据元素自然排序结果,返回给定集合中的最小元素
  • Object min(Collection collection, Comparator comparator):根据 comparator 指定的排序规则,返回给定集合中的最小元素
  • int frequency(Collection collection, Object obj):返回指定集合中指定元素的出现次数
  • void copy(List, dest, List src):将 src 中的内容复制到 dest 中
  • boolean raplaceAll(List list, Object oldVal, Object newVal):使用新值替换 List 集合中所有的旧值

四、泛型(Generic)

集合容器类在设计阶段(声明阶段)不能确定这个容器到底实际存的时什么类型的对象,所以在 JDK5 之前只能把元素类型设计为 Object。JDK5 之后使用泛型来解决

  • 因为这个时候除了元素的类型不确定,其他的部分是确定的,如关于这个元素如何保存,如何管理等。
  • 此次此时将元素的类型设计成一个参数,该类型参数即泛型

4.1、泛型的概念

允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值以及参数类型

  • 这个类型参数在使用时(如:继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)

从 JDK5 后,Java 引入了 "参数化类型(Parameterized type)" 的概念,允许在创建集合时再指定集合元素的类型

  • 如:List<String>,表明该 List 只能保存字符串类型的对象

4.1.1、在集合中使用泛型

JDK5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参

  • 在实例化集合类时,可以指明具体的泛型类型
  • 在集合类或接口中凡是定义类或接口时,内部结构(如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型
    如 add(E e) 实例化后 add(Integer e)
  • 注意:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,可用包装类代替
  • 若实例化时没有指明泛型的类型,默认类型为 java.lang.Object 类型

4.2、自定义泛型结构

4.2.1、自定义泛型类、泛型接口

  • 泛型类可能有多个参数,此时可将多个参数放在一对尖括号内 "<>",如<E1, E2, E3>

  • 泛型类的无参构造器与普通无参构造器一致:public GenericClass(){}

  • 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致

  • 泛型不同的引用不能相互赋值

    • 尽管在编译时 ArrayList<String> 和 ArrayList<Integer> 是两种类型,但在运行时只有一个 ArrayList 被加载到 JVM 中
  • 泛型如果不指定将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object

    • 建议:泛型一旦使用便要从一而终
  • 若泛型结构是一个接口或抽象类,则不能创建泛型类的对象

  • JDK7 新增类型推断,泛型的简化操作:ArrayList<String> list = new ArrayList<>();

  • 泛型的指定中不能使用基本数据类型,可以使用包装类替换

  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型(静态方法的加载要早于类的初始化)

  • 异常类不能是泛型的

  • 不能使用 new E[]

    • 但可使用 E[] elements = (E[])new Object[capacity];
      参考:ArrayList 源码中声明:Object[] elementData,而非泛型参数类型数组
  • 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型

    • 子类不保留父类的泛型:按需实现
      • 没有类型 -> 擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类
      • 全部保留
      • 部分保留
    • 子类除了指定或保留父类的泛型,还可以增加自己的泛型

4.2.2、自定义泛型方法

方法也可被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时参数的类型就是传入数据的类型

泛型方法的格式:访问权限 <泛型> 返回类型 方法名(泛型标志 参数名称) 抛出的异常

public class DAO {
    public <E> E get(int id, E e) {
        E result = null;
        return result;
    }
}

泛型方法:在方法中出现泛型结构,泛型参数与类的泛型参数没有任何关系

  • 泛型方法所属的类是不是泛型类都无关
  • 泛型方法可以声明为静态的
    • 泛型参数是在调用方法时确定的,并非在实例化类时确定

4.3、泛型在继承方面的体现

虽然类 A 是类 B 的父类,但是 E<A> 和 E<B> 二者不具备子、父类关系,二者是并列关系

  • 区别:类 A 是类 B 的父类,A<E> 是 B<E> 的父类

4.4、通配符

泛型可以使用通配符 "?",比如 List<?>,Map<?>

  • List<?> 是 List<String>、List<Object> 等各种泛型 List 的父类
    • 类 A 是类 B 的父类,E<A> 和 E<B> 没有关系,二者共同的父类是 E<?>
  • 读取 List<?> 的对象 list 中的元素时,永远是安全的,因为无论 list 的真实类型,它包含的都是 Object
  • 不可以向 list 中写入(添加元素),因为不知道元素的类型,不能向其中添加对象
    • 唯一的例外是 null,它是所有类型的成员

4.4.1、有限制条件的通配符

  • <?>
    • 允许所有泛型的引用调用
  • 通配符指定上限
    • 上限 extends:使用时指定的类型必须是继承某个类,或者实现某个接口(即小于等于 <=)
    • ? extends A:G<? extends A> 可以作为 G<A> 和 G<B> 的父类,其中 B 是 A 的子类
    • <? extends Number>:(无穷小, Number]:只允许泛型为 Number 及 Number 子类的引用调用
      • 左开右闭:包括右不包括左
    • <? extends Comparable>:只允许泛型为实现 Comparable 接口的实现类的引用调用
  • 通配符指定下限
    • 下限 super:使用时指定的类型不能小于操作的类(即大于等于 >=)
    • ?super A:G<? super A> 可以作为 G<A> 和 G<B> 的父类,其中 B 是 A 的父类
    • <? super Number>:[Number, 无穷大):只允许泛型为 Number 及 Number 父类的引用调用
      • 左闭右开:包括左不包括右

五、java.io

5.1、File 类

java.io.File 类:文件和文件目录路径的抽象表示形式,与平台无关

  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身
    • 若需要访问文件内容本身,则需要使用 IO 流
  • 若要在 Java 程序中表示一个真实存在的文件或目录,则必须有一个 File 对象
    • 但 Java 程序中的一个 File 对象有时并不表示一个真实存在的文件或目录
  • File 对象可作为参数传递给流的构造器

5.1.1、常用构造器

public File(String pathname)

  • 以 pathname 为路径创建 File 对象,可以是绝对路径或相对路径,若是相对路径则默认的当前路径在系统属性 user.dir 中存储

public File(String parent, String child)

  • 以 parent 为父路径,child 为子路径创建 File 对象

Public File(File parent, String child)

  • 根据一个父 File 对象和子文件路径创建 File 对象

5.1.2、路径分隔符

路径中的每级目录之间用一个路径分隔符隔开,路径分隔符与系统相关:

  • Windows 和 DOS 系统默认采用 "\" 表示
  • UNIX 和 URL 使用 "/" 表示

File 类提供常量,根据操作系统动态提供分隔符:

  • public static final String separator

5.1.3、常用方法

File 类的获取功能:

  • public String getAbsolutePath():获取绝对路径
  • public String getPath():获取路径
  • public String getName():获取名称
  • public String getParent():获取上层文件目录路径。若没有则返回 null
  • public long length():获取文件长度(即字节数)。不能获取目录的长度
  • public long lastModified():获取最后一次的修改时间,毫秒值
  • 适用于文件目录:
    • public string[] list():获取指定目录下的所有文件或文件目录的名称数组
    • public File[] listFiles():获取指定目录下的所有文件或文件目录的 File 数组

File 类的重命名功能:

  • public boolean renameTo(File dest):把文件重命名为指定的文件路径
    • fileA.rename(fileB) 重命名成功条件:要求 fileA 真实存在且 fileB 不存在

File 类的创建功能:

  • public boolean createNewFile():创建文件(若此文件存在则不创建,返回 false)
  • public boolean mkdir():创建文件目录(若此文件目录存在则不创建;若此文件目录的上层目录不存在也不创建)
  • public boolean mkdirs():创建文件目录(若此文件目录的上层目录不存在则一并创建)

File 类的删除功能:

public boolean delete():删除文件或文件目录(文件夹)

  • Java 中删除不进入回收站(Windows)
  • 要删除一个文件目录,需要该文件目录内不能包含文件或文件目录

File 类的判断功能:

  • public boolean isDirectory():判断是否是文件目录
  • public boolean isFile():判断是否是文件
  • public boolean exists():判断是否存在
  • public boolean canRead():判断是否可读
  • public boolean canWrite():判断是否可写
  • public boolean isHidden():判断是否隐藏

5.2、IO 流

I/O 是 Input/Output 的缩写,I/O 技术用于处理设备之间的数据传输

  • Java 程序中,对于数据的输入/输出操作以 "流(steam)" 的方式进行
  • java.io 包下提供各种 "流" 类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据

5.2.1、流的分类

按操作数据单位不同:

  • 字节流(8 bit)
  • 字符流(16 bit)

按数据流的流向不同:输入流、输出流

按流的角色不同:节点流、处理流

(抽象基类) 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Weiter

IO 流体系:

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Weiter
访问文件 FileInputStream FileOutputStream FileReader FileWeiter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWeiter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWeiter
访问字符串 StringReader StringWeiter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWeiter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
FilterInputStream FilterOutputStream FilterReader FilterWeiter
打印流 PrintOutputStream PrintWeiter
推回输入流 PushbackInputStream PushbackReader
数据流 DataInputStream DataOutputStream

5.2.2、对象流 —— 序列化、反序列化

用于存储和读取基本数据类型数据或对象的处理流

  • 可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原

序列化:用 ObjectOutputStream 类保存基本类型数据或对象的机制

反序列化:用 ObjectInputStream 类读取基本类型数据或对象的机制

ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量

序列化的特点在于可将任何实现了 Serializable 接口的对象转换为字节数据,使其在保存和传输时可被还原

序列化是 RMI(Remote Method Invoke —— 远程方法调用)过程的参数和返回值都必须实现的机制

若要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,即所涉及的类必须实现 Serializable 接口或 Externalizable 接口之一

凡是实现 Serializable 接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID

  • 其用来表明类的不同版本间的兼容性(对序列化对象进行版本控制,检测对象各版本序列化时是否兼容)
  • 若没有显式定义,Java 运行时环境根据类的内部细节自动生成。若类的实例变量做了修改,SerialVersionUID 可能发生变化,因此建议显式声明
  • Java 的序列化机制通过在运行时判断类的 serialVersionUID 来验证版本一致性。在进行反序列化时,JVM 将传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,若相同便认为是一致的,可以进行序列化,否则出现序列化版本不一致的异常(InvalidCastException)

六、反射(Reflection)

反射被认为是动态语言的关键。反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法

  • 加载完类后,在堆内存的方法区产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象包含了完整的类的结构信息,可以通过这个对象看到类的结构
  • 正常方式:引入需要的 "包.类" 名称 -> 通过 new 实例化 -> 取得实例化对象
  • 反射方式:实例化对象 -> getClass() 方法 -> 得到完整的 "包.类" 名称

反射机制提供的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

反射相关的主要 API:

  • java.lang.Class:代表一个类
  • java.lang.reflect.Method:代表类的方法
  • java.lang.reflect.Field:代表类的成员变量
  • java.lang.reflect.Constructor:代表类的构造器

关于 java.lang.Class 类的理解:

  • 将字节码文件进行解释运行便相当于将某个字节码文件加载到内存中,此过程称为类的加载
  • 加载到内存中的类,称其为运行时类,此运行时类就作为 Class 的一个实例
  • Class 的实例就对应着一个运行时类

6.1、获取 Class 的实例

加载到内存中的运行时类,会缓存一定的时间,在此时间内,可以通过不同的方式来获取此运行时类(唯一的)

  • 调用运行时类的属性:.class
    • Class objClass = Object.class
  • 通过运行时类的对象,调用 getClass()
    • Object obj = new Object();
      class objClass = obj.getClass();
  • 调用 Class 的静态方法:forName(String classPath)
    • Class objClass = Class.forNmae("java.lang.Object");
    • 其中 classPath 以类的全类名方式表示
      • 类的全类名:包含包名在内的类的完整路径
  • 使用类的加载器
    • ClassLoader classLoader = Person.class.getClassLoader();
      Class objClass = classLoader.loadClass("java.lang.Object");

拥有 Class 对象的结构:

  • class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
  • interface:接口
  • []:数组
    • 只要数组的元素类型与维度一样,就是同一个 Class
  • enum:枚举类
  • annotation:注解@interface
  • primitive type:基本数据类型
  • void

6.2、类的加载过程

当程序主动使用某个类时,若该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

  • 类的加载(Load)
    • 将类的 .class 文件读入内存,并为其创建一个 java.lang.Class 对象(此过程由类加载器完成)
  • 类的链接(Link)
    • 将类的二进制数据合并到 JRE 中
  • 类的初始化(Initialize)
    • JVM 负责对类进行初始化

6.2.1、类加载器

类加载器的作用:

  • 类加载作用:将 .class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口
  • 类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收这些 Class 对象

JVM 规范定义了如下类型的类加载器:

  • Bootstrap Classloader:引导类加载器:C++ 编写,JVM 自带类加载器,负责 Java 平台核心类库,用来加载核心类库,该类加载器无法直接获取
  • Extension Classloader:扩展类加载器:负责 jre/lib/ext 目录下的 jar 包或 -D java.ext.dirs 指定目录下的 jar 包装入工作
  • System Classloader:系统类加载器:负责 java -classpath 或 -D java.class.path 所指定的目录下的类与 jar 包装入工作(最常用)

对于自定义类,使用系统类加载器进行加载

  • 调用系统类加载器的 getParent() 获取扩展类加载器
  • 调用扩展类加载器的 getParent() 无法获取引导类加载器
  • 引导类加载器主要负责加载 Java 的核心类库,无法加载自定义类

读取配置文件的两种方式:

  • FileInputStream 方式:此时文件默认在当前 module 下
    • FileInputStream f = new FileInputStream("filePath");
  • ClassLoader 方式:此时文件默认在当前 module 的 src 下
    • InputStream f = classLoader.getResourseAsStream("filePath");

6.3、通过反射创建运行时类的对象

newInstance() 方法:创建对应运行时类的对象(内部调用了运行时类的空参构造器)

  • 运行时类必须提供空参构造器
  • 空参构造器必须有合适的访问权限

JavaBean 中要求提供一个 public 的空参构造器

  • 便于通过反射,创建运行时类的对象
  • 便于子类继承此运行时类,默认调用 super() 时,保证父类有此构造器

6.4、获取运行时类结构的常用方法

获取属性结构:

  • Field[] getFields()
    • 获取当前运行时类及其父类中声明为 public 访问权限的属性
  • Field[] getDeclareFields()
    • 获取当前运行时类中声明的所有属性(不包含父类中声明的属性)
  • 获取属性权限修饰符:
    • int getModifiers()
      • 针对获取到的返回值,可将参数传入 Modifier.toString(int i) 方法查看具体权限名
  • 获取属性数据类型:
    • Class getType()
      • 针对获取到的返回值可调用 getName() 方法查看具体数据类型名
  • 获取属性变量名:
    • getName():针对具体属性可调用

获取方法结构:

  • Method[] getMethods()
    • 获取当前运行时类及其父类中声明为 public 权限的方法
  • Method[] getDeclareMethods()
    • 获取当前运行时类中声明的所有方法(不包含父类中声明的方法)
  • 获取方法声明的注解:
    • Annotation[] getAnnotation():针对具体方法可调用
  • 获取方法权限修饰符:
    • int getModifiers()
      • 针对获取到的返回值,可将参数传入 Modifier.toString(int i) 方法查看具体权限名
  • 获取方法返回值类型:
    • int getReturnType()
      • 针对获取到的返回值可调用 getName() 方法查看具体数据类型名
  • 获取方法名:
    • getName():针对具体方法可调用
  • 获取形参列表:
    • Class[] getParameterTypes():针对具体方法可调用
  • 获取抛出的异常:
    • Class[] getExceptionTypes():针对具体方法可调用

获取构造器结构:

  • Constructor[] getConstructors()
    • 获取当前运行时类中声明为 public 的构造器
  • Constructor[] getDeclaredContructors()
    • 获取当前运行时类中声明的所有的构造器

获取运行时类所在的包:

  • Package getPackage()

获取运行时类声明的注解:

  • Annotation[] getAnnotations()

获取运行时类实现的接口:

  • Class[] getInterfaces():

获取运行时类的父类:

  • Class getSuperclass():获取当前运行时类的父类
  • 获取运行时类的父类实现的接口:
    • Class[] getSuperclass().getInterfaces()
  • 获取运行时类的带泛型的父类:
    • Class getGenericSuperclass()
  • 获取运行时类的带泛型的父类的泛型:
    • Type[] ((ParameterizedType) classObj.getGenericSuperclass()).getActualTypeArguments()
      • 可针对具体的 Type[] 中的元素调用 getName() 查看其名称
        • result[0].getName();
        • ((Class) result[0]).getName();

6.5、操作运行时类中的指定结构

6.5.1、操作运行时类中的指定的属性

  • Field getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
  • setAccessible(true):设置当前属性可访问
  • Object get(Object obj):获取指定对象的此属性值
  • set(Object obj, Object value):设置指定对象的此属性值

操作静态属性:

  • get(当前运行时类.class) 或 get(null)
  • set(当前运行时类.class, Object value) 或 set(null, Object value)

6.5.2、操作运行时类中指定的方法

  • Method getDeclaredMethod(String methodName, Class<?>... parameterTypes):获取指定的某个方法
  • setAccessible(true):设置当前方法可访问
  • Object invoke(Object obj, Object... args):调用获取到的指定的某个方法
    • 第一个参数:方法的调用者(当前运行类的某个实例)
    • 第二个参数:对应的形参列表
  • invoke 的返回值即为对应类中调用的方法的返回值

调用静态方法:

  • invoke(当前运行时类.class) 或 invoke(null)

6.5.3、操作运行时类中指定的构造器

  • Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):获取指定的构造器
    • 参数指明构造器的参数列表
  • setAccessible(true):设置当前构造器可访问
  • T newInstance(Object ... initargs):调用此构造器创建运行时类的对象

6.6、动态代理

6.6.1、代理模式的原理

使用一个代理将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理类。代理对象决定是否以及何时将方法调用转到原始对象上

静态代理:代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展,同时每一个代理类只能为一个接口服务,这样程序开发中必然产生过多的代理

动态代理:指通过代理类来调用其他对象的方法,并且是在程序运行时根据需要动态的创建目标类的代理对象

  • 动态代理的使用场合:
    • 调式
    • 远程方法调用
  • 动态代理相对于静态代理的优点
    • 抽象角色(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样可以更加灵活和统一的处理众多的方法

实现动态代理需要解决的问题:

  • 根据加载到内存中的被代理类,动态的创建一个代理类及其对象
  • 当通过代理类的对象调用方法时,动态的去调用被代理类中的同名方法

实现动态代理用到的方法:

  • public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • 实现 InvocationHandler 接口

七、Java8 新特性

7.1、Lambda 表达式

可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)

  • 更简洁、更灵活 —— 更紧凑的代码风格

7.1.1、Lambda 表达式的使用

格式:

  • ->:lambda 操作符或箭头操作符
  • -> 左侧:lambda 形参列表
    • 即接口中的抽象方法的形参列表
  • -> 右侧:lambda 体
    • 即重写的抽象方法的方法体

Lambda 表达式的表现形式:

-> 左侧:lambda 形参列表的参数类型可以省略(类型推断):如果 lambda 形参列表只有一个参数,则 () 也可以省略

-> 右侧:lambda 体应该使用一对 {} 包裹;若 lambda 体只有一条执行语句(可能是 return 语句),则可以省略(需要同时)这一对 {} 和 return

  • 无参,无返回值
    • Runnable r = () -> System.out.println(1);
  • Lambda 需要一个参数,但没有返回值
    • Consumer<String> con = (String str) -> System.out.println(str);
  • 数据类型可省略("类型推断":可由编译器推断得出)
    • Consumer<String> con = (str) -> System.out.println(str);
  • Lambda 若只需要一个参数时,参数的 () 可以省略
    • Consumer<String> con = str -> System.out.println(str);
  • Lambda 需要两个或以上的参数,多条执行语句,并且有返回值
    • Comparator<Integer> com = (x, y) -> {System.out.println(x, y); return Integer.compare(x, y);};
  • 当 Lambda 体只有一条语句时,return 与 {} 若存在则都可以省略(return 与 {} 需要同时省略)
    • Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

Lambda 表达式的本质:作为函数式接口的实例

7.2、函数式接口

只包含一个抽象方法的接口,称为函数式接口

  • 可以通过 Lambda 表达式来创建该接口的对象
    • 若 Lambda 表达式抛出受检异常(非运行时异常)该异常需要在目标接口的抽象方法上进行声明
  • 可以在一个接口上使用 @FunctionInterface 注解,可以检测它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口

7.2.1、如何理解函数式接口

Java 不但可以支持 OOP 还可以支持 OOF(面向函数编程)

在函数式编程语言中 Lambda 表达式的类型是函数,但在 Java8 中,Lambda 表达式是对象,它们必须依附于一类特别的对象类型 —— 函数式接口

  • Java8 中,Lambda 表达式就是一个函数式接口的实例(只要一个对象是函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示)
    • 匿名实现类可以用 Lambda 表达式来写

Java 内置四大核心函数式接口:

函数式接口 参数类型 返回类型 用途
Consumer<T>
消费型接口
T void 对类型为 T 的对象应用操作,包含方法:void accept<T t>
Supplier<T>
供给型接口
T 返回类型为 T 的对象,包含方法:T get()
Function<T,R>
函数型接口
T R 对类型为 T 的对下对象应用操作,并返回结果。
结果是 R 类型的对象。包含方法:R apply(T t)
Predicate<T>
断定型接口
T boolean 确定类型为 T 的对象是否满足某约束,并返回 boolean 值。
包含方法:boolean test(T t)

7.3、方法引用与构造器引用

7.3.1、方法引用

当要传递给 Lambda 体的操作已经有实现的方法时,可以使用方法引用

  • 本质上就是 Lambda 表达式,而 Lambda 表达式是函数式接口的实例,因此方法引用也是函数式接口的实例

  • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致

  • 格式:使用操作符 "::" 将类(或对象)与方法名分隔开表示

  • 常见情况:

    • 对象 :: 非静态方法
      • 对应 Consumer 中的 void accept(T t)
    • 类 :: 静态方法
      • 对应 Comparator 中的 int compare(T t1, T t2)
        Integer 中的 int compare(T t1, T t2)
    • 类 :: 非静态方法
      • 对应 Comparator 中的 int compare(T t1, T t2)
        String 中的 int t1.compareTo(t2)

7.3.2、构造器引用

与方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致,抽象方法的返回值类型即为构造器所属的类的类型

  • Supplier 中的 T get()
  • Function 中的 R apply(T t)
  • BiFunction 中的 R apply(T t, U u)

数组引用:把数组看作是一个特殊的类,则写法与构造器引用一致

八、Stream API

Stream 与集合:

  • Stream 关注的是数据的运算 —— 与 CPU 相关

  • 集合关注的是数据的存储 —— 与内存相关

Stream 的特点:

  • Stream 自己不存储数据
  • 不会改变原对象,而是返回一个持有结果的新 Stream
  • Stream 操作是延迟执行的,将等到需要结果时才执行

Stream 执行流程:

  • Stream 的实例化
  • 中间操作
  • 终止操作
  • 说明:
    • 一个中间操作链对数据源的数据进行处理
    • 一旦执行终止操作,就执行中间操作链并产生结果。之后不会再被使用

8.1、Stream 的实例化

  • 通过集合:Java8 中 Collection 接口被扩展,提供了两个获取流的方法
    • default Stream<E> stream():返回一个顺序流(串行流)
    • default Stream<E> parallelStream():返回一个并行流
  • 通过数组:Java8 中 Arrays 的静态方法 stream() 可以获取数组流
    • static <T> Stream<T> stream(T[] array):返回一个流
    • 重载形式,能够处理对应基本类型的数组
      • public static IntStream stream(int[] array)
      • public static LongStream stream(long[] array)
      • public static DoubleStream stream(double[] array)
  • 通过 Stream 的 of() 方法:可以调用 Stream 类静态方法 of() 通过显式值创建一个流。可以接收任意数量的参数
    • public static<T> Stream<T> of(T... value):返回一个流
  • 创建无限流:可以使用静态方法 Stream.iterate() 和 Stream.generate() 创建无限流
    • 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    • 生成:public static<T> Stream<T> generate(Supplier<T> s)

8.2、Stream 的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理,而在终止操作时一次性全部处理,称为 "惰性求值"

  • 筛选与切片:
    • filter(Predicate p):接收 Lambda,从流中排除某些元素
    • distinct():筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
    • limit(long maxSize):阶段流,使其元素不超过给定数量
    • skip(long n):跳过元素,返回一个丢弃了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
  • 映射:
    • map(Function f):接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
    • map ToDouble(ToDoubleFunction f):接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
    • map ToInt(ToIntFunction f):接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
    • map ToLong(ToLongFunction f):接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
    • flatMap(Function f):接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
  • 排序:
    • sorted():产生一个新流,其中按自然排序排序
    • sorted(Comparator com):产生一个新流,其中按比较器顺序排序

8.3、Stream 的终止操作

终止操作会从流水线生成结果。其结果可以是任何不是流的值(List、Integer,甚至是 void)

流进行了终止操作后不能再次使用

  • 匹配与查找:
    • allMatch(Predicate p):检查是否匹配所有元素
    • anyMatch(Predicate p):检查是否至少匹配一个元素
    • noneMatch(Predicate p):检查是否没有匹配所有元素
    • findFirst():返回第一个元素
    • findAny():返回当前流中的任意元素
    • count():返回流中元素总数
    • max(Comparator c):返回流中最大值
    • min(Comparator c):返回流中最小值
    • forEach(Consumer c):内部迭代(使用 Collection 接口需要自定义迭代称为外部迭代)
  • 规约:
    • reduce(T iden, BinaryOperator b):可以将流中元素反复结合起来得到一个值,返回 T
    • reduce(BinaryOperator b):可以将流中元素反复结合起来得到一个值,返回 Optional<T>
    • map 和 reduce 的连接通常称为 map-reduce 模式
  • 收集:
    • collect(Collector c):将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法
    • Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)
    • Collectors 实用类提供了一些静态方法,可以方便地创建常见收集器实例
      • toList:把流中元素收集到 List
        • 返回值类型:List<T>
      • toSet:把流中元素收集到 List
        • 返回值类型:Set<T>
      • toCollection:把流中元素收集到创建的集合
        • 返回值类型:Collection<T>
      • counting:计算流中元素的个数
        • 返回值类型:Long
      • summingInt:对流中元素的 Integer 属性求和
        • 返回值类型:Integer
      • averagingInt:计算流中元素 Integer 属性的平均值
        • 返回值类型:Double
      • summarizingInt:收集流中 Integer 属性的统计值(如平均值)
        • 返回值类型:IntSummaryStatistics
      • joining:连接流中每个字符串
        • 返回值类型:String
      • maxBy:根据比较器选择最大值
        • 返回值类型:Optional<T>
      • minBy:根据比较器选择最小值
        • 返回值类型:Optional<T>
      • reducing:从一个作为累加器的初始值开始,利用 BinaryOperator 与流中元素逐个结合,从而归约成单个值
        • 返回值类型:归约产生的类型
      • collectionAndThen:包裹另一个收集器,对其结果转换函数
        • 返回值类型:转换函数返回的类型
      • groupingBy:根据某属性值对流分组,属性为 K,结果为 V
        • 返回值类型:Map<K, List<T>>
      • partitioningBy:根据 true 或 false 进行分区
        • 返回值类型:Map<Boolean, List<T>>

九、Optional

Optional<T> 类(java.util.Optional) 是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在

  • 之前使用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念,并且可以避免空指针

创建 Optional 类对象的方法:

  • Optional.of(T t):创建一个 Optional 实例,t 必须非空
  • Optional.empty():创建一个空的 Optional 实例
  • Optional.ofNullable(T t):创建一个 Optional 实例,t 可以为 null

判断 Optional 容器中是否包含对象:

  • boolean isPresent():判断是否包含对象
  • void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传给它

获取 Optional 容器的对象:

  • T get():如果调用对象包含值,返回该值,否则抛出异常
  • T orElse(T other):如果调用对象包含值,返回该值,否则返回指定的 other 对象
  • T orElseGet(Supplier<? extends T> other):如果调用对象包含值,返回该值,否则返回 Supplier 接口实现提供的对象
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果调用对象包含值,返回该值,否则抛出由 Supplier 接口实现提供的异常
posted @ 2020-11-22 23:48  DMCs95  阅读(153)  评论(0编辑  收藏  举报