• 第02篇_进阶语法

    第01章_泛型

    第一节 泛型简介

    1. 什么是泛型?

    泛型是计算机编程中一种重要的思维方式,它将程序算法数据类型相分离,使得同一套程序算法能够应用于各种数据类型,并且可以保证类型安全,提高可读性。

    通俗来说,泛型就是类型参数化,即通过参数的形式传入类型,将代码与具体的数据类型解绑,同一套代码可用于多种数据类型。

     

    2. 泛型的本质

    泛型的本质是类型擦除(这对后面理解泛型非常重要)。在编译过程中,所有的泛型都将会被替换为Object类(或其上界类),并在合适的位置插入必要的强制类型转换,虚拟机只能执行这种转换后的非泛型代码

     

    3. 泛型的好处

    前面得知,泛型类最后依旧会被转换为非泛型类,那么我们使用泛型类有什么好处呢? 主要有两点:

    1. 避免强制类型转换,做到在编译期进行类型安全检查,防止类型转换异常(ClassCastException)。

    2. 精简代码,增强代码的健壮性和可维护性。

     

     

    第二节 泛型的基本使用

    泛型根据定义的位置不同,分为泛型类泛型接口泛型方法三类。

     

    1. 泛型类

     

    2. 泛型接口

     

    3. 泛型方法

    注意:

    1. 如果方法为静态方法,那么将不能够使用类上声明的泛型(静态变量同理),因为他们是类级别共享的。

    2. 一个方法是不是泛型的,与它所在的类是不是泛型没有任何关系,可以使用类上的泛型,也可以新定义方法的泛型。

     

    4. 泛型的类型信息

    虽然泛型在编译时被擦除为Object或上界类,但是在运行时,泛型引用的对象是实际的不同具体类型,并且可以获取和使用该类型信息。

     

     

    第三节 限定泛型

    1. 无限定泛型的使用限制

    在上述案例中,声明泛型时未做任何额外的限制,因此在泛型具体化时,可以使用任意类型,这种未被限制的泛型称为无限定泛型。也正是因为在具体化时没有限制类型的取值范围,因此无限定泛型在使用时将会受到一些限制

    例如,在通过泛型引用E e操作指向的对象时,由于E可能是任意类型,因此只能调用任意类型的根类Object的属性或方法。

    为了减弱上述限制,我们可以在声明泛型时进一步限定泛型可具体化的类型范围,要求其必须继承某类或实现某个接口,这样就可以在保证类型安全的前提下使用该类(接口)的一系列方法了,这种被限制可具体化类型范围的泛型称为限定泛型

     

    2. 限定为某类(接口)或其子类

    上文提到的Pair<U,V>类,对其进行扩展,限定泛型可具体化的类型必须是Number或其子类,格式为:<泛型名 extends 上界类名>

    注意:

    1. 对于限定泛型,在进行类型擦除时,将转换为它的上界类。

    2. 上界接口可以存在多个,如:T extends Base & Comparable & Serializable,其中Base为上界类,其它为上界接口。

     

    2. 上界类为泛型类

    上界类也可以是一个带泛型的泛型类,那么在声明限定泛型时,必须对上界类的泛型进行具体化:

    注意:

    1. 在实例化泛型类时,如果泛型的具体类型省略,将默认为Object类型,但是在具体化上界类时,并非如此。

     

    3. 上界类为其它泛型

    上界类还可以是已声明的其它泛型,当该泛型被具体化时,才会确定上界类的具体类型。如上述的DynamicArray的addAll()方法:

    如果不使用T extends E将会怎样?即addAll()方法定义如下所示,可以看到,将会出现编译错误。

    为什么会出现编译错误呢?我们分析下,如果DynamicArray<Integer>能给DynamicArray<Number>赋值将会怎么样?

    注意:

    1. 在add()方法中,形参为E,类型擦除后转换为Number,可以传入Number及Integer等子类;

    2. 但是在addAll()方法中,形参为DynamicArray<E>,类型擦除后为DynamicArray<Number>,而DynamicArray<Integer>是不允许传给DynamicArray<Number>的,否则将会出现上述隐患;

     

     

    第四节 泛型通配符

    泛型具体化时(而非声明时),支持一些通配符的使用,它可以通配多种具体类型,但同时也带来了一些限制,下面将会详细介绍。

     

    1. 通用泛型通配符

    通用泛型通配符用于在具体化泛型时通配所有的具体类型,它简化了泛型的声明和使用,格式为:?

    相应的,由于具体类型未知,因此在使用被通配的泛型对象时,也有一些限制。

    为减弱上述限制,根据不同的使用场景,提供了两种特定通配范围的泛型通配符:子类型泛型通配符和超类型泛型通配符。

     

    2. 子类型泛型通配符

    子类型泛型通配符对通配的具体类型范围做出了一些限制,用于通配ParentClass其子类,格式为:? extends ParentClass

    在得知子类型泛型通配符只通配某个类及其子类后,那么就可以确定它的上界类了,上界类确定后就可以使用上界类的属性和方法,并且可以赋值给上界类。(注意:该例中上界类为E,同样是一个未知类型,因此没有其它额外的方法可以调用,同样也只能够赋值给E)

     

    3. 超类型泛型通配符

    超类型泛型通配符和子类型泛型通配符相反,它用于通配ChildClass及其父类,格式为:? super ChildClass

    在得知超类型泛型通配符只通配某个类及其父类后,那么就可以确定它的下界类了,下界类确定后就可以使用下界类作为引用

    注意:关于限定泛型、子类型通配符、超类型通配符的赋值兼容

     

    再来看另外一个关于超类型通配符的使用场景:

     

    4. 限定泛型和泛型通配符对比

     

     

    第五节 泛型的局限性

    前面提到,Java中的泛型是通过类型擦除来实现的,所有的泛型在编译时都会被替换为Object或上界类,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性,其中有的部分是比较容易理解的,有的则是非常违反直觉的 。

     

    1. 泛型的具体化类型不能是基本类型

    泛型的具体化类型不能是基本类型,应该使用它的包装类。

     

    2. 不能通过泛型直接创建对象

    不能通过泛型直接创建对象,需要传入泛型对应的类型信息,通过反射创建。

    提示:

    1. 实际上,可以参考第二节中,通过泛型引用的对象获取Class信息,进而创建对象。

     

    3. 泛型类的不同具体化本质上还是同一个类

    泛型在编译时将会被擦除为Object(或上界类),不同的具体化类型只是在编译时自动插入了不同的强制类型转换,本质上还是同一个类。

    由于是同一个类,因此它们的类型信息完全一致,并且类上所具有的静态资源也是共享的

    注意:

    1. 内部的first/second编译时都被擦除为Object类型,但是运行时分别指向不同的具体化类型对象。

     

    4. 类上的泛型不能用于静态变量或静态方法

    泛型类的泛型不能用于静态变量或静态方法,应为静态方法单独声明泛型,而静态变量不允许为泛型

     

    5. 类型擦除可能会引发一些冲突

     

    6. 不能直接创建泛型数组

    如下创建泛型数组的代码是禁止的:

    因为数组是Java直接支持的概念,它知道数组元素的实际类型,在类型不对时可快速触发运行时异常,因此编译时允许赋值给父类数组。

    但是如果允许创建泛型数组,如下:

    由于Pair<Double, String>和Pair<Object, Integer>的类型都是Pair,因此第二行赋值时即不会编译报错,也不会立即触发运行时异常,埋下了隐患,因此Java禁止创建泛型数组。

    如果我们非要创建泛型类型的数组,可以使用原始类型来创建,这样可以跳过编译检查,但是问题还是存在的。

    最好的解决办法是,使用泛型容器来代替泛型数组

     

    7. 泛型容器不能直接转换为数组

    有时候我们希望将泛型容器直接转化为一个泛型数组,如下:

    实现toArray()方法时,一般是先创建一个泛型数组,然后拷贝数据再返回该数组。

    由于前面已经提到,直接创建泛型数组是行不通的:E[] arr = new E[size]; // err,因此,可能会想到如下两种方式:

    虽然这两者方式没有编译错误,但是在运行时都会抛出如下异常:

    要想实现上述需求,必须知道数组元素的类型信息,才能创建泛型数组,可以修改实现如下:

    提示:

    1. 实际上,可以通过一些运行时类型信息来获取元素的类型信息,从而传入Array.newInstance创建数组,可以对比第二节相关案例。

    第02章_容器

    第一节 容器类概述

    1. 容器体系简介

    容器类主要分为集合类容器(Collection)映射类容器(Map)。集合类容器包括列表(List)队列(Queue)集合(Set)三大类,其中队列又衍生出双端队列(Deque),它们都是容器类的超级接口,并且一般都定义了对应的抽象类。

    在日常开发中,我们一般使用上述接口或抽象类的具体子类,常用的容器如下:

    容器容器类说明
    数组列表ArrayList基于数组实现的列表
    链式列表LinkedList基于链表实现的列表,也可作为链式双端队列
    数组双端队列ArrayDeque基于循环数组实现的双端队列
    链式双端队列LinkedList基于链表实现的双端队列,也可作为链式列表
    优先级队列PriorityQueue基于实现的单端队列,元素可以按优先级出列
    哈希集合HashSet基于哈希表+链表(或红黑树)实现的无序集合
    带链的哈希集合LinkedHashSet继承自HashSet,在其基础上通过额外的来维护插入有序
    树状集合TreeSet基于红黑树实现的规则有序集合
    枚举集合EnumSet基于数组实现的高效集合,只适用于枚举类型元素
    哈希映射HashMap基于哈希表+链表(或红黑树)实现的无序映射
    带链的哈希映射LinkedHashMap继承自HashMap,在其基础上通过额外的来维护存取有序
    树状映射TreeMap基于红黑树实现的规则有序映射
    枚举映射EnumMap基于位向量实现的高效映射,只适用于枚举元素

    注意:

    1. 容器一般会继承对应的抽象类及直接实现对应的超级接口,如ArrayList继承了AbstractList,并且还直接实现了List接口。

    2. 但是也有些例外,如ArrayDeque没有对应的AbstractDeque,EnumSet和EnumMap没有直接实现对应的Set和Map接口等。

     

    2. 常见接口和抽象类简介

    1) Iterable<T>和Iterator<E>

    Iterable<T>接口表示“可迭代的”,它提供了获取迭代器(Iterator<E>)的方法,通过迭代器可以进行遍历操作,并且支持ForEach语法。

    ListIterator<E>扩展了Iterator接口,增加了一些向前遍历、添加元素、修改元素、返回索引位置等方法。

    提示

    1. 只要对象实现了Iterable接口,就可以使用foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。

     

    2) Collection<E>与AbstractCollection<T>

    Collection<E>表示单列集合,只定义了基本的增删改查和遍历等方法,没有定义元素间的顺序或位置,也没有规定是否有重复元素。

    注意:

    1. Collection的add方法默认为抛出UnsupportedOperationException异常。

     

    3) 列表相关接口和抽象类

    List<E> 是 Collection<E> 的子接口,表示有顺序和位置的集合,增加了根据索引位置进行操作的方法。

     

    4) 队列相关接口和抽象类

    Queue<E>是Collection<E>的子接口,表示先进先出的队列,在尾部添加,从头部查看或删除

    Deque<E>是Queue<E>的子接口,表示更为通用的双端队列,有明确的在头或尾进行查看、添加和删除的方法。

     

    5) 集合相关接口和抽象类

    Set<E>是Collection<E>的子接口,它没有增加新的方法,但保证不含重复元素。SortedSet<E>和NavigableSet<E>在Set的基础上进行了扩充,方便实现TreeSet子类。

     

    6) 映射相关接口和抽象类

    Map<K,V>表示键值对集合(映射),它的元素为Entry<K,V>类型,经常根据键进行操作。SortedMapMap<K,V>和NavigableMapMap<K,V>在Map的基础上进行了扩充,方便实现TreeMap子类。

     

    3. 容器使用注意事项

    1) 根据容器特性选择合适的容器

    不同类型的容器有不同的适用场景,如数组类容器适合随机访问,链式容器适合头尾存取,堆类型容器适合TopN问题,树状容器适合元素按规则排序的场景等,应该根据使用场景选用合适的容器。

     

    2) 本章节介绍的容器都是线程不安全的

    除了HashtableVectorStack外,我们本章介绍的各种容器类都是线程不安全的。如需多线程操作同一个容器,可以使用Collections工具类提供的synchronizedXXX方法对容器对象进行同步,或者使用专门的线程安全容器类。

     

    3) 容器在通过迭代器遍历时会检测结构性变化

    容器类提供的迭代器都有一个特点,会在迭代时检测容器的结构性变化(通过modCount来实现),如通过容器引用去添加或删除元素等,将会抛出ConcurrentModificationException。如确实需要增删元素,可以通过迭代器的add和remove方法操作。

     

     

    第二节 列表(List)

    1. 数组列表(ArrayList)

    ArrayList<E>是List<E>的子类,基于数组实现,它的随机访问效率很高,但从中间插入和删除元素需要移动元素,效率比较低

     

    1) ArrayList常用方法

    注意:

    1. 基于索引操作的方法,在操作节点前都会检查索引是否越界,如果越界将会抛出IndexOutOfBoundsException。

    2. 基于索引操作的插入类方法,当索引为0时,插入到头部,索引为size()时,插入到尾部

    3. 基于索引操作的删除和查看方法,索引范围必须为0~size()-1;

     

    2) ArrayList实现原理

    ArrayList内部使用数组elementData来存储元素,默认长度为10,长度会随着元素个数的变化动态分配(1.5倍),一般会有一些预留的空间,由另外一个整数size来记录实际的元素个数。

     

    3) 注意事项:并发修改异常

    由于迭代器内部会维护一些索引位置相关的数据,因此要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了,就会抛出ConcurrentModificationException。所谓结构性变化,就是添加和删除元素等,只是修改元素内容不算结构性变化。

    如何避免异常呢?可以使用迭代器的remove方法,或直接通过list.removeIf来实现相同功能。

     

    4) 扩展:迭代器的实现原理

    为什么上面可以使用迭代器的remove方法来删除呢?这涉及到迭代器的实现原理,它内部维护了三个成员变量:

    当外部类调用add、remove等影响结构性的方法时,modCount都会自增,而每次迭代器操作的时候都会检查expectedModCount是否与外部类的modCount相同,这样就能检测出结构性变化。

    如果使用迭代器的remove方法,它在调用ArrayList的remove方法时,可以同步更新内部的cursor、lastRet和expectedModCount的值,因此可以正确删除。不过,需要注意的是,调用迭代器的remove方法前必须先调用next,否则会抛出IllegalStateException。

    注意:

    1. 迭代器是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历相分离,是一种常见的设计模式。

    2. 迭代器语法更加简洁,并且对于部分容器,性能更加高效,推荐优先使用

     

     

    2. 链式列表(LinkedList)

    1) LinkedList常用方法

    LinkedList<E>是List<E>的间接子类,基于链表实现,随机访问效率比较低,但增删元素只需要调整邻近节点的链接。此外,它还继承了Deque\<E\>接口,可以用作双端队列先进先出队列等。

    注意:

    1. 栈/队列是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过使用不同的名称和方法,概念上更为清晰。

    2. offer/poll/peek开头的方法在已满或为空时返回false或null(虽然LinkedList没有”已满“的概念,但其它队列/栈可能会有)。

    3. add/remove/get和push/pop/element开头的方法在已满或为空时会抛出IllegalStateException或NoSuchElementException。

     

    2) LinkedList实现原理

    LinkedList是一个双端链表,每个元素(节点)在内存中单独存放,元素之间通过前驱指针后继指针进行链接。

    而LinkedList内部只需保存一个头指针和一个尾指针即可,分别指向第一个节点和最后一个节点,通过指针寻址操作,关联所有元素,构成逻辑上的双端链表。

     

     

    第三节 队列(Queue/Deque)

    1. 数组队列(ArrayDeque)

    ArrayDeque<E>是Deque<E>的子类,基于循环数组实现,它可以用作双端队列先进先出队列等。和链式双端队列相比,从两端操作的效率会更高一些,但是不支持索引操作,并且在中间插入和删除很慢

     

    1) ArrayDeque常用方法

    构造方法如下,其它常用方法和LinkedList中介绍的类似,不再赘述。

     

    2) ArrayDeque实现原理

    下面重点看下ArrayDeque的循环数组是如何实现的,ArrayDeque内部主要有如下实例变量:

    通过引入头指针尾指针使物理上的简单数组(从头到尾)变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。头尾有四种分布:

    队列的长度始终可以通过(tail - head) & (elements.length - 1)算出。而在添加新元素时,如在尾部添加,则tail = (tail + 1) & (elements.length - 1),如在头部添加,则head = ( head-1 ) & ( elements.length-1 ),如果出现head==tail,则表示容器已满,需要将容量扩为之前的2倍

    注意:

    1. ArrayDeque中,有效元素不允许为null,contains等方法在内部遍历时也将null视为结尾。

    2. 通过位与运算,可以有效提高计算下标的效率,并且可以确保索引不会越界,这在循环数组中的应用非常常见。

     

     

    2. 链式队列(LinkedList)

    LinkedList<E>还继承了Deque<E>接口,可以用作双端队列、先进先出队列、栈等,在链式列表章节已有介绍。

     

     

    3. 优先级队列(PriorityQueue)

    PriorityQueue<E>是Queue<E>的子类,表示优先级队列,基于实现的。常见的应用场景有“求前K个最大的元素”、“求实时中值”等。

     

    1) PriorityQueue常用方法

     

    2) PriorityQueue实现原理

    优先级队列基于实现,而堆是一颗完全二叉树,在从左到右并分层进行编号后,可以直接计算出任意节点的父节点和左右子节点的编号,如编号为i的节点,其父节点编号为i/2,左右子节点的编号分别为2\*i2\*i+1,可以将这个编号作为数组的索引,将每个节点按编号存储在一个连续的数组中,不仅节省空间,而且访问效率非常高。

    image-20230226181241753

    但在插入和删除(即将尾部元素覆盖头部元素)元素时,需要进行向上调整(siftup)或向下调整(siftdown)来维持堆的性质,效率都为Olog2N。

    注意:

    1. 堆分为小顶堆和大顶堆,大顶堆指每个元素不大于其父元素根节点就是最大节点,元素之间可以重复,小顶堆与之类似。

     

     

    第四节 映射(Map)

    1. 哈希映射(HashMap)

    HashMap<K,V>是Map<K,V>的子接口,基于哈希表实现(哈希表+链表/红黑树),要求元素的键(key)重写hashCode和equals方法,操作效率很高,但元素间没有顺序。

     

    1) HashMap常用方法

     

    2) HashMap实现原理

    HashMap内部有一个Node类型的数组table,称为哈希表(哈希桶),每个元素(table[i])指向一个单向链表(或红黑树)。

    当put新元素时,先计算key对应的hash值,再通过取余( h%(length-1),可优化为h&(length-1) )得到数组中的索引位置buketIndex,然后将value存放在该位置或该位置指向的链表(或红黑树)中。

    image-20230226214123166

     

     

    2. 带链的哈希映射(LinkedHashMap)

    LinkedHashMap<K,V>继承自HashMap<K,V>,在其哈希表+链表(或红黑树)的基础上额外添加了一条用于维护元素顺序的双向链表,这个链表可以按插入顺序排序,也可以按访问顺序排序。

     

    1) LinkedHashMap常用方法

    构造方法如下,其它方法和HashMap类似,但是get/put等方法内部会额外维护一个插入或访问顺序,同时遍历时按照该顺序进行。

    提示:

    1. 如果键本来就是有序的,使用LinkedHashMap比TreeMap效率更高。

     

    2) LinkedHashMap实现原理

    LinkedHashMap是HashMap的子类,内部增加了如下实例变量:

    其中Entry继承了HashMap.Node,增加了两个变量before和after, 分别指向前驱节点和后继节点。

    当处于“插入有序”模式时,哈希表新增元素的同时,也会添加到链表的末尾。当处于“访问有序”模式时,无论是插入、修改或访问,都会将该节点移到链表的末尾。

     

    3) 应用:LRU缓存

     

     

    3. 树状映射(TreeMap)

    TreeMap<K,V>是Map<K,V>的间接子接口,基于排序二叉树(红黑树)实现,要求键(key)实现Comparable<E>接口,或者创建TreeSet时提供一个Comparator<E>对象,其操作效率稍低,但键(key)可以按比较有序。

     

    1) TreeMap常用方法

    构造方法如下,其它方法和HashMap类似。此外,还有一些继承自SortedMap和NavigableMap的方法,由于使用较少,请查阅API文档。

    注意:

    1. TreeMap使用键的比较结果(而非equals)对键进行排重,即使键实际上不同,但只要比较结果相同,就会被认为相同。

     

    2) TreeMap实现原理

    TreeMap是基于红黑树实现的,主要成员变量如下:

     

    4. 枚举映射(EnumMap)

    EnumMap<K,V>是Map<K,V>的子接口,使用比哈希表效率更高的静态数组实现,但是要求元素必须为枚举类型。

     

    1) EnumMap常用方法

    构造方法如下,需要通过枚举类的Class信息进行构造,同时key必须为枚举类型

    下面是一个简单的使用示例:

    注意:

    1. EnumMap是有顺序的,为枚举元素定义的顺序

    2. 当put的值为null时,将会被替换为EnumMap.NULL存储,而值为真正的null表示该key不存在。

    3. 上述两种场景在get时都会返回null,但是在遍历时,不存在的key将会被跳过,如:{SMALL=null, MEDIUM=中}。

    4. 虽然使用普通的HashMap可以实现相同的功能,但是使用EnumMap更加简洁安全和高效。

     

    2) EnumMap实现原理

    EnumMap内部有两个长度相等的静态数组,一个表示所有可能的键, 一个表示对应的值,值为 null 表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。

     

     

    第五节 集合(Set)

    1. 哈希集合(HashSet)

    HashSet<E>是Set<E>的子接口,基于HashMap<E,Object>实现,因此同样要求元素的键(key)重写hashCode和equals方法, 特性也基本类似,如访问效率高,元素间没有顺序等;

     

    1) HashSet常用方法

     

    2) HashSet实现原理

    HashSet的内部有一个HashMap,操作基本都是委托其完成的。

     

     

    2. 带链的哈希集合(LinkedHashSet)

    LinkedHashSet<E>继承自HashSet<E>,基于LinkedHashMap<K,V>实现,默认支持插入有序,不支持访问有序。

     

    1) LinkedHashSet常用方法

    构造方法如下,其它常用方法和HashSet的使用类似,但add等方法内部会额外维护一个插入顺序,同时遍历时按照该顺序进行。

     

    2) LinkedHashSet实现原理

    LinkedHashSet继承自HashSet,构造时内部的map被初始化为LinkedHashMap,因此支持按插入有序:

     

     

    3. 树状集合(TreeHashSet)

    TreeSet<E>是Set<E>的间接子接口,基于TreeMap<E,Object>实现,, 特性也基本类似,同样也要求元素的键实现Comparable<E>接口,或者创建TreeMap时提供一个Comparator<E>对象。

     

    1) TreeHashSet常用方法

    构造方法如下,其它常用方法和HashSet中介绍的类似,不再赘述。此外,有一些继承自SortedSet和NavigableSet的方法,由于使用较少,请查阅API文档。

     

    2) TreeSet实现原理

    TreeSet的内部有一个NavigableMap,操作基本都是委托其完成的。

     

     

    4. 枚举集合(EnumSet)

    EnumSet<E>是Set<E>的子接口,基于位向量实现,效率非常高,但是元素要求必须为枚举类型。

     

    1) EnumSet常用方法

    构造函数如下,其它方法和HashSet使用类似。

    一个简单的使用示例如下:

     

    2) EnumSet实现原理

    EnumSet与之前介绍的Set实现类不同,它内部没有用对应的Map类EnumMap,而是使用了一种极为高效的位向量方式。

    位向量就是用一个位表示一个元素的状态(是否存在),用一组位表示一个集合的状态。如前面的枚举类型Day,它有7个枚举值,可以用一个字节的低7位表示,最高位补0,当对应元素存在时,则置为1,否则为0。

    image-20230227195421219

    当枚举类型的枚举值个数<=64时,将创建RegularEnumSet实现类,内部采用64位的long类型存储元素是否存在的信息。否则将创建JumboEnumSet实现类,采用long类型的数组存储,并用size记录元素的个数。

    在进行一些增删改查时,基本都是使用位操作来进行的,因此效率非常高,部分操作如下:

    扩展:取补集时为什么要移除高位多余的1?

    因为elements是64位的,当前枚举类可能没有用那么多位,取反后高位部分都变为了1,因此需将超出universe.length的部分设为0。

    在移动位数为负数的情况下,上述代码相当于:elements &= -1L >>> (64-universe.length)。如universe.length 为 7,则 -1L>>> ( 64-7 ) 就是二进制的 1111111,与 elements 相与,就会将超出universe.length部分的高 57 位都变为0。

     

     

    第六节 相关工具类

    1. Collections

    Collections工具类以静态方法的方式提供了很多通用算法和功能。

     

    1) 对容器进行操作

    针对容器接口的通用操作,这是面向接口编程的一种体现,是接口的典型用法。

     

    2) 返回一个容器

    目的是为了使更多类型的数据更为方便和安全地参与到容器类协作体系中。

    注意:为什么使用了泛型后还会有类型安全问题呢? 因为Java是通过擦除来实现泛型的,类型参数是可选的,并且JDK5前的老代码都没有泛型。

    3) 其它

    第03章_异常

    第一节 异常类

    1. 异常类简介

    异常指程序运行过程中出现的错误,以java.lang.Throwable为根,Java定义了非常多的异常: image-20221111181541923

    Throwable:是所有异常的基类。它有两个主要子类:java.lang.Errorjava.lang.Exception

    特殊的,Exception有一个子类叫做RuntimeException,实际含义表示未受检异常(unchecked exception),相对而言,Exception的其它子类称为受检异常(checked exception)。 未受检异常不要求程序对可能抛出的异常进行处理,使用更加方便。

     

     

    2. 自定义异常类

    应用程序可以通过继承Exception或其子类创建自定义异常。特别的,如果继承的是RuntimeException,那么创建的将会是未受检异常。

     

     

    第二节 异常处理

    1. 抛出异常(throw)

    throw用来抛出一个异常对象,并将这个异常对象传递到调用者处,并结束当前方法的执行。

     

    2. 捕获异常(try…catch)

    异常抛出后,会沿着方法栈往调用者传递,我们可以对其进行捕捉和处理。

    异常捕捉后,可以获取异常相关的信息,如下:

    如需捕捉多个异常,则可以按照如下格式书写,注意越明确的类型应越先捕捉

    注意:

    1. 如果异常一直未被捕捉,最后会被Java虚拟机处理,默认行为是打印堆栈信息,然后退出线程

     

     

    3. 声明异常(throws)

    对于受检异常,如果未在当前方法进行捕捉,则必须通过throws关键字在方法上进行声明,提醒调用者处理异常。

    注意:

    1. 子类方法不能声明或抛出父类方法中未声明的异常。

    2. 你可以声明抛出异常,但实际并不抛出,这一般用在在父类方法,方便子类进行扩展。

     

     

    4. finally代码块

    try后面还可以跟finally语句,finally内的代码不管有无异常发生,都会执行,一般用于释放资源,如数据库连接、文件流等。

    注意

    1. 如果程序被突然终止(宕机、断电等)或在try/catch中调用了退出JVM相关的方法,则finally代码块不会被执行。

    2. 如果某些资源即使在程序退出后也不能自动释放,则不能依赖finally代码块,如持久化存储的业务标记。

    3. 如果finally代码块中有return语句或抛出异常,则会覆盖try代码块中的返回结果,应避免该情况。

     

     

    5. try-with-resources(JDK7+)

    try-with-resources语句配合java.lang.AutoCloseable接口,可以实现资源的自动关闭(基于finally代码块实现)。

     

    6. try-with-resources(JDK9+)

    在Java 9之前,资源必须声明和初始化在try语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是final的或者是事实上final的(即虽然没有声明为final但也没有被重新赋值)。

     

     

    第三节 异常相关扩展

    1. 异常链

    在catch代码块中可重新抛出异常,异常可以是原来的,也可以是新建的,并且可以关联原来的异常形成异常链。

    上述案例中,捕捉到NumberFormatException异常后,转化为统一的BizException重新抛出,并将exception作为cause传递给了新建的BizException,这样就形成了一个异常链,捕获到BizException的代码可以通过getCause()得到底层的NumberFormatException。

    某些Java的异常类并没有定义带cause的构造方法,但可以通过Throwable的Throwable initCause(Throwable cause)方法来设置cause,但是必须注意,该方法只能被调用一次。

     

     

    2. 异常与枚举结合

     

     

    3. 特殊情况下的finally

    如果在try或者catch语句内有return语句,则return语句执行后的结果先会缓存,待finally语句执行结束后才返回(但是该值不能被改变)。

    如果在finally中也有return语句呢? 那么try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样。

    同理,如果finally代码块中抛出了异常,则原返回值或异常也将会被掩盖。

    因此,应该尽量避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

    第04章_文件

    第一节 文件概述

    1. 基础概念

    文件:文件是操作系统对磁盘数据的抽象,方便用户进行数据管理。

    文件存储:文件在磁盘上以二进制形式进行存储,根据解读方式的不同,可分为UTF-8文本文件、JPG图片文件、MP4视频文件、ZIP压缩文件等多种类型,一般以后缀名进行标识。

    文本文件:如果文件能以某种编码(UTF-8、GBK等)映射为可读的字符形式,那么该类文件称为文本文件。文本文件具有换行的概念,在Windows系统中使用\r\n这2个字节表示换行符(Linux为\n,MAC系统为\r)。

    注意:

    1. 在Windows系统中,文件名是大小写不敏感的,即同目录下的a.txt和A.txt是同一个文件。

    2. 文件IO比较慢,且需经过内核态和用户态的两次复制,因此文件操作时一般按块进行,并设置一定大小的缓冲区。

     

    2. 文件与目录(File)

    2.1 File类

    java.io.File 类封装了操作系统和文件系统的差异,提供了统一的文件和目录API。它可以表示文件,也可以表示目录,构造方法如下:

    File 类中有 4 个静态变量, 表示路径或目录的分隔符

     

    2.2 文件基本信息

    注意, File 对象没有返回创建时间的方法 , 因为创建时间不是一个公共概念 , Linux/Unix 就没有创建时间的概念。

     

    2.3 文件安全与权限

    File 类中与安全和权限相关的主要方法有:

     

    2.4 常用文件操作

    当 File 对象代表文件时,主要操作有创建 、 删除 、 重命名等。

     

    2.5 常用目录操作

    当 File 对象代表目录时,可以执行目录相关的操作,如创建、遍历等。

     

    2.6 文件和目录操作案例

     

     

    第二节 字节流

    在Java中,将文件及其它输入输出设备抽象为,并构建了基于流的相关协作体系,默认情况下,流为字节形式,称为字节流

     

    1. InputStream/OutputStream

    InputStream/OutputStream(抽象类)表示最顶层的字节输入流和字节输出流,其中定义了它们的一些共性方法:

     

    2. FileInputStream/FileOutputStream

    FileInputStream/FileOutputStream继承自InputStream/OutputStream, 表示文件输入流文件输出流,即输入输出目的地为文件。

    下面是一些按字节读写文件的示例:

     

    3. ByteArrayInputStream/ByteArrayOutputStream

    ByteArrayInputStream/ByteArrayOutputStream也继承自InputStream/OutputStream, 表示字节数组输入流字节数组输出流,即输入输出目的地为字节数组。

    下面示例将从文件输入流读取数据到字节数组输出流,然后转化为字符串输出。

     

    4. DataInputStream/DataOutputStream

    DataInputStream/DataOutputStream是装饰类基类FilterInputStream/FilterOutputStream的子类,并且实现了DataInput/DataOutput接口,可以以各种基本类型和字符串读取或写入数据

    下面是一个使用DataInputStream/DataOutputStream装饰FileInputStream/FileOutputStream后,用来序列化对象的使用示例:

     

    5. ObjectInputStream/ObjectOutputStream

    ObjectInputStream/ObjectOutputStream继承自InputStream/OutputStream,并实现了ObjectInput/ObjectOutput接口,可以读取和写入实现了java.io.Serializable接口的对象

    下面是一个使用ObjectInputStream/ObjectOutputStream来写入和读取对象的示例:

    实际上,List以及之前介绍的String、Date、Double、Map等, 都实现了Serializable接口,上述示例可以再次简化:

    扩展:

    1. ObjectInput/ObjectOutput是DataInput/DataOutput的子接口,增加了Object readObject()void writeObject(Object obj)方法。

     

    6. BufferedInputStream/BufferedOutputStream

    BufferedInputStream/BufferedOutputStream也是装饰类基类FilterInputStream/FilterOutputStream的子类,它提供了对流进行缓冲的作用,提升操作流的性能。

    在使用FileInputStream/FileOutputStream时,应该几乎总是在它的外面包上对应的缓冲类,如下所示:

     

    7. RandomAccessFile

    如果需要对文件进行随机读写重复读,可以使用RandomAccessFile,它一个更接近于操作系统API的封装类。

    注意:

    1. 虽然RandomAccessFile有类似于读写字节流的方法,但大多是实现DataInput/DataOutput接口而来,并不是InputStream/OutputStream的子类。

     

    8. MappedByteBuffer

    如果需要处理大型文件或在不同应用程序之间共享数据,可以使用MappedByteBuffer,它是文件映射到内存的字节数组,操作该字节数组即可操作文件,大多数操作系统都支持该机制,称为内存映射文件

    内存映射文件基于FileInputStream/FileOutputStream或RandomAccessFile,它们都有一个获取FileChannel方法,而FileChannel可以将文件映射到内存,映射完成后,文件就可以关闭了,后续对文件的读写可以通过MappedByteBuffer完成。

    注意:

    1. 映射模式受限于文件打开的方式,若是输入流或写模式打开文件,则不能设置为READ_WRITE映射模式。

    2. 内存映射文件仅在发生实际读写时,才会将要读写的部分按页映射到内存。数据读写完毕后,由操作系统进行同步,只要操作系统不崩溃,一定可以同步到磁盘上,即使应用程序已经退出。

    3. 在该种方式下,程序直接访问内核内存空间,仅需一次数据拷贝过程,比普通文件读写的性能更高

    4. 内存映射文件也有局限性,比如,它不太适合处理小文件,它是按页分配内存的,对于小文件,会浪费空间,另外,映射文件要消耗一定的操作系统资源,初始化比较慢。

    MappedByteBuffer代表内存中的字节数组,是 ByteBuffer(Buffer) 的子类,它可以简单理解为一个字节数组包装类,这个字节数组的长度是不可变的,在内存映射文件中,这个长度由map方法中的参数size决定。

    内存映射文件的另一个重要特点是,它可以被多个不同的应用程序共享,多个程序可以映射同一个文件,映射到同一块内存区域,一个程序对内存的修改,可以让其他程序也看到,这使得它特别适合用于不同应用程序之间的通信

     

    9. 字节流操作实用方法

     

     

    第三节 字符流

    字符流以字符为单位读取和解读流中的字节,一个字符可能包含多个字节,这取决解读时使用的字符编码。

    注意:对于增补字符集,一个完整的字符内容可能需要两个字符(char)来表示。

     

    1. Reader/Writer

    Reader/Writer(抽象类)表示最顶层的字符输入流字符输出流,其中定义了它们的一些共性方法:

     

    2. InputStreamReader/OutputStreamWriter

    InputStreamReader/OutputStreamWriter是适配器类,继承自Reader/Writer,能将字节流(InputStream/OutputStream)转换为字符流(Reader/Writer)

    下面是一个将字节流适配为字符流并进行字符读写的示例:

     

    3. FileReader/FileWriter

    FileReader/FileWriter继承自Reader/Writer, 表示文件字符输入流文件字符输出流,即输入输出目的地为文件。

    注意:

    1. FileReader和FileWriter以及下面介绍的几种字符流操作类,都不能直接指定编码类型,只能使用默认编码。

    2. 如需指定字符流的编码类型,可以使用适配器InputStreamReader/OutputStreamWriter将字节流转换为指定编码的字节流。

     

    4. CharArrayReader/CharArrayWriter

    CharArrayReader/CharArrayWriter也继承自Reader/Writer, 表示字符数组输入流字符数组输出流,即输入输出目的地为字符数组。

    下面是一个从文件字符流中读数据到字节数组输出流的示例:

     

    5. StringReader/StringWriter

    StringReader/StringWriter也继承自Reader/Writer, 表示字符串输入流字符串输出流,即输入输出目的地为字符串。它与CharArrayReader/CharArrayWriter类似,只是输入源为String,输出目标为StringBuffer。实际上,String和StringBuffer内部是由char数组组成的,所以它们本质上是一样的。

     

    6. BufferedReader/BufferedWriter

    BufferedReader/BufferedWriter是装饰类,直接继承自Reader/Writer,提供缓冲以及按行读写的功能。

    注意:

    1. 通过System.lineSeparator()也可以获取平台特定的换行符。

    2. FileReader/FileWriter是没有缓冲的,也不能按行读写, 因此一般应该在它们的外面包上对应的缓冲类。

    下面是一个带缓冲的文件字符流的读写示例:

     

    7. PrintWriter/PrintStream

    PrintWriter继承自Writer,是一个非常方便的类,可以直接指定文件名/File/OutputStream/Writer等作为构造参数,还可以指定编码类型,支持自动缓冲,可以自动将多种类型转换为字符串,在输出到文件时 ,可以优先选择该类。

    注意:

    1. 如果以Writer为参数的构造方法,则PrintWriter就不会包装BufferedWriter了,其它类型参数则会。

    下面是使用PrintWriter改造上面写学生信息的示例:

    PrintStream继承自FilterOutputStream,属于字节流,但其功能与PrintWriter非常的相似。一些差异点如下:

     

    8. Scanner

    Scanner是一个单独的类,它是一个简单的文本扫描器,能够从流中提取基本类型和字符串。

    使用Scanner改造上面解析每行学生信息的示例如下:

     

    9. 标准流

    操作系统在启动时通常会打开三个标准流:

    System.in:标准输入流(InputStream),一般指键盘,可以和Scanner配合使用,从键盘输入数据。

    System.out:标准输出流(PrintStream),一般指控制台,输出提示信息。

    System.err:标准错误流(PrintStream),一般也是控制台,输出错误信息,如使用e.printStackTrace()打印异常信息。

    标准流可以重定向,如将标准输入流重定向到文件,从文件中接受输入,或将标准输出流(错误流)重定向到文件,将输出写到文件。

    标准输入输出流也是操作系统的重要协作机制,命令从标准输入接受参数,处理结果写到标准输出,这个标准输出可以连接到下一个命令作为标准输入,构成管道式的处理链条。

     

    10. 字符流操作实用方法

     

     

    第四节 常见文件类型处理

    1. Properties

    Properties文件一般用于配置程序的属性参数,每一行表示一个属性,属性是以等号(=)或冒号(:)分隔的键值对,如下例所示:

    Java中有一个专门的类Properties来处理该类属性文件,它会自动忽略文件中的空行和注释行(#或!开头)以及分隔符前后的空格。

    一个使用Properties加载属性文件并获取属性的示例如下:

    值得注意的是,Properties不能直接处理中文,在配置文件中,所有非ASCII字符需要使用Unicode编码,如name=老马需替换为name=\u8001\u9A6c。如果你使用IDE进行编辑,或许它会帮你自动转换,不过,你也可以使用JDK命令:native2ascii -encoding UTF-8 native.properties ascii.properties进行转换。

     

    2. CSVFormat/CSVPrinter

    CSV(Comma-Separated Values)文件一般用于表示表格类型的数据,每一行表示一条记录,记录包含多个字段,字段之间用逗号、制表符、冒号、分号等分隔。

    如果字段内容包含分隔符或换行符等特殊字符,主要有两种方式处理:

    1. 方式一:使用特殊符号如双引号(")将字段内容括起来,如果字段内容有",则用两个"表示。

    2. 方式二:使用转义字符如反斜杠()对特殊字符进行转义,如果字段内容有\,则用两个\表示,如hello\, world \\ abc'n"老马"

    CSV文件需要处理转义字符、空格、null值以及注释等复杂情形,可以采用Apache Commons CSV库来解析CSV文件,导入依赖如下:

    解析CSV文件主要依赖CSVFormat类,有一些预定义的格式,如CSVFormat.DEFAULTCSVFormat.RFC4180等,也可以通过如下一些方法自定义CSVFormat对象。

    写CSV文件,可以使用 CSVPrinter 类,它有许多打印相关的方法:

    下面是一个读取和写入CSV文件的示例:

     

    3. Workbook(Excel)

    Excel是广泛使用的表格文档格式,通常使用POI类库来进行处理,主要的类如下:

    类名说明
    WorkbookExcel文件(接口),HSSFWork-book和XSSFWorkbook实现类分别表示.xls文件和.xlsx文件
    Sheet工作表
    Row数据行
    Cell单元格

    使用POI类库前先导入对应的依赖如下:

    下面是一个简单的Excel文件读取和写入示例:

     

    4. Jsoup(HTML)

    Jsoup是一种常用的HTML分析器,Maven依赖如下:

    下面是使用Jsoup解析URL的示例:

     

    5. GZIPOutputStream/GZIPInputStream

    Java内置了gzipzip两种压缩格式的支持,其中gzip只能压缩一个文件,而zip文件中可以包含多个文件。

    压缩和解压gzip文件使用GZIPOutputStreamGZIPInputStream装饰类,它们分别继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream)。

     

    6. ZipOutputStream/ZipInputStream

    压缩和解压zip文件使用ZIPOutputStreamZIPInputStream装饰类,也继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream),但是使用起来稍微复杂些。

     

    第五节 扩展:关于JDK序列化

    1. JDK序列化简介

    序列化就是将对象转化为字符流/字节流反序列化就是将字符流/字节流转化为对象,主要有两个用途:一个是对象持久化;另一个是跨网络的数据交换和远程过程调用。

    在标准JDK中,通过ObjectInputStream`ObjectOutputStream流提供了基于java.io.Serializable接口的序列化机制。 它有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便地进行定制,处理版本问题等,但它也有一些重要的局限性:

    由于这些局限性,在跨语言的数据交换格式中,经常采用XML或JSON格式,它们清晰易读,各种语言基本都支持,缺点是性能和序列化大小。在性能和序列化大小敏感的领域,往往会采用更为精简高效的二进制方式,如ProtoBuf、Thrift、MessagePack等 。

    注意:

    1. 如果尝试序列化未实现Serializable接口的对象,那么将会抛出java.io.NotSerializableException

    2. 如果 a、b 两个对象都引用同一个对象 c ,序列化后c 只会保存一份 , 并且反序列化后依然指向相同对象。

    3. 如果 a 、 b 两个对象有循环引用,即 a 引用了 b , 而 b 也引用了 a,反序列化后依然 可以保持引用关系。

     

    2. 配置JDK序列化

    默认的序列化机制将对象中的所有字段保存和恢复,但某些字段信息,如对象的创建时间,默认hashcode()返回值等并不需要保存,我们可以将字段声明为 transient,则默认的序列化机制将会忽略它。如 LinkedList 中的这些字段:

    之后,我们可以在类中定义 writeObject/readObject 方法来自己保存该字段。

    如 LinkedList 的序列化和反序列化代码如下:

     

    3. 关于类的版本

    默认情况下,Java根据类中一系列的信息自动生成一个版本号, 如果类的定义发生了变化 , 版本号就会变化,如果反序列化时的版本号不一致,则会抛出java.io.InvalidClassException

    我们可以手动在类中添加如下静态变量来标识类的版本,而非由Java自动生成,以便更好地控制序列化的版本和节省性能。

    如果版本号一致,但实际的字段不匹配,Java 会分情况自动进行处理 , 以尽量保持兼容性。

    第05章_注解

    第一节 注解简介

    1. 什么是注解?

    注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等,注解可以被编译器、程序运行时、和其他工具使用,用于增强或修改程序行为等

     

    2. 注解的本质

    注解本质上就是一个接口,该接口默认继承Annotation接口,我们使用javap将生成的注解class文件反编译后,可以看到如下内容:

     

     

    3. 元注解

    元注解是一种用于修饰注解的注解,常用的元注解如下:

     

    1) @Target

    @Target表示注解的目标,取值为一个或多个ElementType枚举值。如果没有声明@Target,默认为适用于所有类型。

     

    2) @Retention

    @Retention表示注解信息保留到什么时候,取值为一个RetentionPolicy枚举值。如果没有声明@Retention,默认为CLASS。

     

    3) @Inherited

    @Inherited表示注解将会被子类继承。如下示例中,Child类并没有直接声明Test注解,但依然检测其存在。

     

    4) @Documented

    @Documented表示将注解信息包含到Javadoc中。

     

    5) Repeatable

    @Repeatable表示可以在同一个地方多次应用该注解。

     

     

    4. 内置注解

    1) @FunctionInterface

    可修饰接口,用于检查被标注的接口是否为函数式接口(只有一个抽象方法的接口)。

     

    2) @Override

    可修饰方法,表示该方法是“重写”方法,可以减少编程错误(如父类方法名修改后,若子类方法名忘记修改,存在注解时将报错)。

     

    3) @Deprecated

    可修饰方法字段参数等,表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的。

     

    4) @SuppressWarnings

    可修饰方法等,用于压制Java的编译警告,通过必填参数设置压制的类型。

     

     

    第二节 自定义注解

    1. 注解格式

    注解的定义和接口类似,格式如下:

     

    2. 注解参数

    注解本质上就是一个接口,定义注解参数即在接口中定义抽象方法,其中方法名表示参数名,返回值类型表示参数的类型

    注意:

    1. 参数的类型必须为如下类型:基本类型(不包括包装类型)、StringClass枚举注解,以及这些类型的数组

    2. 参数可以通过default关键字指定默认值,默认值必须为一个常量,不能为null

    3. 如提供了参数,但未指定默认值,则必须在使用注解时提供具体的值(不能为null)。

     

     

    第三节 使用和解析注解

    1. 使用注解

    查看元注解@Target的参数值,明确注解可使用的位置,然后在目标位置添加注解并填充参数。

    只有一个参数,且名称为value时,提供参数值时可以省略"value="。

    数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}可以省略。

     

    2. 解析注解

    注解只是对程序的标识,创建注解后,我们应同时提供处理这些标识的其它代码,以使添加的注解生效。我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。

    一个简单的示例如下:

     

     

    第四节 注解应用案例

    1. 定制序列化

     

    2.DI容器

     

    3. DI容器-支持单例

     

    4. 简单测试框架

    第06章_反射

    第一节 反射的概念

    1. 什么是反射?

    一般来说,在操作某个数据的时候,我们都是知道并且依赖于数据的类型的,并且编译器也是根据其类型,进行代码的检查和编译。如:

    1. 根据类型使用new创建对象。

    2. 根据类型定义变量,类型可能是基本类型、类、接口或数组。

    3. 将特定类型的对象传递给方法。

    4. 根据类型访问对象的属性,调用对象的方法等。

    但是反射不一样,它是在运行时(而非编译时)动态获取类型的信息,如接口信息、成员信息、方法信息、构造方法信息等。这些信息使用Class<T>类进行封装,获取Class类后就可以创建对象、访问和修改成员、调用方法等。

     

    2. 获取Class类

    在Java中,每个已加载的类在内存都有一份类信息,使用Class类进行封装,每个对象都有指向它所属类信息的引用。获取方法如下:

    特殊的,基本类型没有getClass()方法,但也都有对应的Class对象,Class的类型参数为对应的包装类型:

    void作为特殊的返回类型,也有对应的Class:

    对于数组每种类型及每个维度都有对应数组类型的Class对象:

    有了Class对象后,我们就可以了解到关于类型的很多信息,并基于这些信息采取一些行动,下面会分组进行介绍。

    注意:

    1. 同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

     

    3.Class的种类

    Class对象代表的类型既可以是普通的类,也可以是内部类,还可以是基本类型、数组等,可以通过以下方法进行区分 :

     

    4. 慎用反射

    反射虽然是灵活的,但一般情况下,并不是我们优先建议的,主要原因是:

    简单的说,如果能用接口实现同样的灵活性,就不要使用反射。

    另外,反射也不是万能的,有些信息无法通过反射获取,如类字段的顺序,方法的参数名称(需要手动在编译时开启 -parameters 参数)等,有些信息即使反射获得后也不能使用,如Unsafe.getUnsafe()方法,在业务代码中是不能调用的。

     

     

    第二节 反射信息

    1. 类信息

    Class有如下方法,可以获取与类名称有关的信息:

    类名称之间的不同可参考如下表格:

    image-20230302100903584

    关于数组类型getName()返回值的说明

    1. 格式为:数组维度+数据类型,其中数组维度用[表示,有几个[表示是几维数组。

    2. 数据类型可以是基本类型,有:boolean(Z), byte(B), char(C), short(S), int(I), long(J), float(F), double(D)。

    3. 数据类型也可以是引用类型(类或接口等),用L+全类名+;表示。

     

    2. 类字段信息

    类中定义的静态变量实例变量都被称为字段,字段信息用Field类封装,可通过Class类的如下方法获取:

    获取Field类对象后,即可通过其方法获取字段信息及修改字段内容。

    注意:

    1. 对于静态变量,get/set方法的obj参数直接传null即可。

    2. private字段不允许直接调用get/set方法,需要先setAccessible(true)关闭Java的检查机制,否则会抛IllegalAccessException。

    3. 如果字段值为基本类型,get/set会自动在基本类型与对应的包装类型间进行转换。

     

    3. 类方法信息

    类中定义的静态方法实例方法都被称为方法,用Method类封装,可通过Class类的如下方法获取:

    获取Method对象后,即可通过其方法获取方法信息及调用方法等。

    关于invoke方法的使用有如下几点注意事项:

    1. 对于静态方法,invoke时obj参数直接传null即可。

    2. invoke方法的参数args可以为null,也可以为一个空数组,返回值被包装为Object类型。

    3. 如果目标方法调用抛出异常,将会被包装为InvocationTargetException重新抛出,可以通过getCause方法得到原异常。

    下面是一个使用invoke调用静态方法的示例:

     

     

    4. 关于修饰符

    获取修饰符时,得到的是一个int类型,可通过Modifier类的如下方法进行解析:

     

     

    第三节 反射操作

    1. 创建对象

    获取类信息(Class)后,可以使用其获取构造器和创建对象

     

    2. 类型检查和转换

    前面介绍过,instanceof关键字可以用来判断引用指向的实际对象类型,但是instanceof后面的类型是在代码中确定的,如果要检查的类型是动态的,可以使用Class类的isInstance方法,效果是一样的:

    isInstance判断的是对象和类之间的关系,Class还有一个方法isAssignableFrom可以判断类与类之间的关系:

    在程序中也往往需要进行强制类型转换,而强制转换到的类型要在写代码时就知道的,如果是动态的,可以封装为如下toType方法:

    3. 类的加载

    Class有三个重载静态方法,可以根据类名加载类:

    其中className与Class.getName()的返回值是一致,如加载String类型的一维数组使用[java.lang.String;

    需要注意的是,基本类型不支持forName方法:

     

    第四节 反射扩展

    1. 反射与数组

    对于数组类型的Class,有一个专门的方法,可以获取它的元素类型:

    另外,java.lang.reflect包中专门提供了一个针对数组反射操作的类Array,以便于统一处理多种类型的数组,主要方法有:

     

    2. 反射与枚举

    对于枚举类型的Class,有一个专门方法 , 可以获取所有的枚举常量:

     

    3. 反射与内部类

    对于内部类类型的Class,也有一些特殊的方法:

     

    4. 反射与泛型

    虽然泛型在运行时会被擦除,但在类信息Class中仍然有关于泛型的一些信息,可以通过反射获取。

    其中Type是一个接口,Class实现了Type,Type的其他子接口还有:

    一个简单的使用示例如下:

     

    5. 反射与注解

     

     

    第五节 应用示例

    1. SimpleMapper

    如下示例使用反射实现一个简单的通用序列化/反序列化类SimpleMapper:

     

    第六节 Unsafe类

    在Java的底层源码中,存在一个sun.misc.Unsafe类,也可以用于创建对象。

    由于它的构造方法是私有的,也没有暴露外部对象,因此只能通过反射来获取,示例如下:

    必须注意的是,他直接调用的底层C++代码,跳过了Java的对象管理和内存管理以及垃圾回收等机制,不会调用Java类的构造方法(可能突破单例模式限制),并且可能造成内存泄漏,因此请谨慎使用。

    注意:

    1. 不能在业务代码中调用Unsafe.getUnsafe(),将会抛出SecurityException,因为该方法被@CallerSensitive注解。

    第07章_函数式编程

    第一节 Lambda表达式

    1. Lambda表达式引入

    接口常作为方法的形参来传递代码,如Collections.sort方法的Comparator类型参数:

    它真实需要的不是一个Comparator对象,而是在对象的int compare(T o1, T o2)方法中包含的大小比较逻辑。

    但由于无法直接传递代码,因此只能传递一个具有该功能的对象。在Java 8之前,最简洁的方式是使用匿名内部类构建一个对象:

    在Java8,引入了Lambda表达式,它是一种紧凑的代码传递方式,传递代码不再有实现接口的模板代码,而是直接给出了方法的实现代码,变得更为直观。

    注意:

    1. 虽然Lambda表达式非常简洁,但它只支持函数式接口,其它的接口类型还需使用匿名内部类。

     

    2. Lambda表达式语法

    Lambda表达式由->分隔为两部分,前面()内是方法的参数列表,后面{}内是方法的实现代码。其中参数列表由函数式接口的抽象方法决定,必须保证参数的类型和顺序完全一致,但参数名称不做要求。

    编译器会尽可能的对Lambda表达式进行推断,以简化其书写:

    注意:

    1. 如需在Lambda表达式中访问局部变量,则该变量必须是final的,或等效final的(因为该变量是通过构造参数直接传入)。

    2. Lambda表达式不是匿名内部类的语法糖,它是基于invokedynamic指令实现的,并不会生成很多类。

     

    3. Lambda使用场景

     

     

    第二节 函数式接口

    1. 什么是函数式接口?

    函数式接口只有一个抽象方法的接口,一般使用@FunctionalInterface进行注解(非强制,与@Override注解类似)。

     

    2. 预定义函数式接口

    Java 8 预定义了大量的函数式接口,用于常见类型的代码传递,这些函数定义在java.util.function包下,主要的有:

    img

    对于基本类型boolean/int/long/double,为避免装箱和拆箱,Java 8 提供了一些专门的函数。比如,int相关的主要函数有:

    img

     

    3. 函数式接口使用示例

    为便于举例,我们定义一个简单的学生类Student,以及一个Student列表:

     

    1) Predicate

     

    Predicate接口提供了and/or/negate三个方法用于组合其它Predicate。

     

    2) Function

     

    Function提供了andThen/compose分别用于后置/前置组合其它Function。

    注意:

    1. 组合后,前面函数式接口的输出必须兼容后面函数式接口的输入,如one.compose(two)是错误的,Integer不能赋值给String。

     

    3) Consumer

     

    Consumer提供了andThen用于后置组合其它Consumer。

     

     

    第三节 方法引用

    1. 什么是方法引用

    方法引用是Lambda表达式的进一步简化。前面说到,Lambda表达式用于传递一段代码,如果这段代码在其它地方已经存在,则可以通过类名/变量名::方法名的格式直接引用。

     

    2. 引用静态方法

    可以通过类名引用静态方法,要求被引用的方法和抽象方法的形参列表及返回值完全一致。

     

    3. 引用构造方法

    也可以通过类名引用构造方法,和引用静态方法要求相同,即被引用的方法和抽象方法的形参列表及返回值完全一致。

     

    4. 引用实例方法

    1) 通过类名

    可以通过类名引用实例方法,但由于实例方法必须通过实例变量调用,因此只能引用抽象方法第一个形参类型中的实例方法,并且剩余形参列表和返回值也要求和引用方法完全一致。

    在运行时,抽象方法第一个参数不作为引用方法的参数传入,而用于调用该引用方法。

     

    2) 通过变量名引用

    通过变量名引用它的任意实例方法,它将通过该变量进行调用。

    注意:

    1. 这个变量名也可以是super或者this

     

     

    第四节 流式编程

    流式编程通常是对集合数据进行处理,让集合中的对象像水流一样流动,分别进行去重过滤映射等操作,就像生产线一样。

    对此,Java 8 引入了一套新的类库,位于包java.util.stream下,称之为Stream API。它有如下一些特征:

     

    1. 获取流

    1)从集合获取流

    Stream API的主要操作定义在Stream接口中,他类似于一个功能更加丰富的迭代器,可以通过Collection接口(JKD8+)的默认方法获取:

    注意:

    1. 顺序流采用单线程处理,并行流并行处理,线程个数一般与系统的CPU核数一样,以充分利用CPU的计算能力。

    2. 并行流的实现基于Java 7引入的fork/join框架,处理由fork和join两个阶段组成,fork就是将要处理的数据拆分为小块,多线程按小块进行并行计算,join就是将小块的计算结果进行合并。

     

    2) 从数组获取流

    Arrays有一些stream方法,可以将数组或子数组转换为流,比如:

     

    3) 构建流

    Stream有一些静态方法,可以构建流:

    一些简单示例如下:

     

    4) 合并流

    如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

     

     

    2. 中间操作

    中间操作(intermediate operation) 不触发实际的执行,用于构建流水线,返回的是Stream对象。

     

    1) filter

    过滤不符合条件的元素。

     

    2) map

    将元素转换为其它类型。

    map函数接受的参数是一个Function<T, R>,为避免装箱/拆箱,提高性能,Stream还有如下返回基本类型特定流的方法:

    DoubleStream/IntStream/LongStream是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码可以为:

     

    3) distinct

    过滤重复的元素,只留下其中一个,是否重复是根据equals方法来比较的。

    虽然都是中间操作,但distinct与filter/map是不同的,filter/map都是无状态的,对于流中的每一个元素,它的处理都是独立的,处理后即交给流水线中的下一个操作。

    但distinct不同,它是有状态的,在处理过程中,它需要在内部记录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。

    对于顺序流,内部实现时,distinct操作会使用HashSet记录出现过的元素,如果流是有顺序的,需要保留顺序,会使用LinkedHashSet。

     

    4) sorted

    对流中的元素进行排序,要求元素实现Comparable接口或传入一个Comparator。

    与distinct一样,sorted也是一个有状态的中间操作,在处理过程中,需要在内部记录出现过的元素,与distinct不同的是,每碰到流中的一个元素,distinct都能立即做出处理,要么过滤,要么马上传递给下一个操作,但sorted不能,它需要先排序,为了排序,它需要先在内部数组中保存碰到的每一个元素,到流结尾时,再对数组排序,然后再将排序后的元素逐个传递给流水线中的下一个操作。

     

    5) skip/limit

    skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流。limit限制流的长度为maxSize,用它们组合可以截取第n+1 ~ n+maxSize的元素。

    skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。

    limit的一个特点是,它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作被称为短路操作

     

    6) peek

    peek主要目的是支持调试,可以使用该方法观察在流水线中流转的元素。

     

    7) flatMap

    接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换为一个流Stream,然后把新生成流的每一个元素传递给下一个操作,完成了一个1到n的映射。

    相应的,针对基本类型,flatMap还有如下类似方法:

     

    3. 终端操作

    终端操作(terminal operation) 触发实际执行,返回具体结果。

     

    1) max/min

    返回流中的最大值/最小值,值的注意的是,它的返回值类型是Optional<T>,而不是T,表示可能返回null(在流中不含任何元素的情况下)。

     

    2) count

    返回流中元素的个数。

     

    3) allMatch/anyMatch/noneMatch

    接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件,它们的区别是:

    如果流为空,这几个函数的返回值都是true

    这几个操作都是短路操作,都不一定需要处理所有元素就能得出结果,比如,对于allMatch,只要有一个元素不满足条件,就能返回false。

     

    4) findFirst/findAny

    返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。

     

    4) forEach/forEachOrdered

    接受一个Consumer,对流中的每一个元素,传递元素给Consumer,区别在于,在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。

     

    5) toArray

    将流转换为数组。

     

    6) reduce

    代表归约或折叠,即将流中的元素归约为一个值,完成n到1的映射。它有三个重载形式,使用它们可以实现max/min/count等函数:

    第一个基本等同于调用:

    比如,使用reduce求分数最高的学生(max),代码可以为:

    第二个reduce函数多了一个identity参数,表示初始值,它基本等同于调用:

    第一个和第二个reduce的返回类型只能是流中元素的类型,而第三个更为通用,它的归约类型可以自定义

    另外,它多了一个combiner参数,combiner用在并行流中,用于合并子线程的结果,对于顺序流,它基本等同于调用:

    注意与第二个reduce函数相区分,它的结果类型不是T,而是U。

    以上,可以看出,reduce虽然更为通用,但比较费解,难以使用,一般情况,应该优先使用其他函数。

     

     

    4. 收集器

    1) 基本原理

    在之前的代码中,如过滤得到90分以上的学生列表:

    最后的collect方法是如何将Stream转换为List<Student>的呢?先看下collect方法相关的定义:

    对于顺序流,collect内部与这些接口方法的交互大概是这样的:

    Collectors.toList()具体是什么呢?看下代码:

    也就是说,collect(Collectors.toList())背后的伪代码如下所示:

     

    2) 容器收集器

     

     

    3) 字符串收集器

    除了将元素流收集到容器中,另一个常见的操作是收集为一个字符串。

     

    5. 分组收集器

    分组类似于SQL语句中的group by子句,它将元素流中的每个元素进行分组,然后针对分组进行处理和收集。

     

    1) 简单分组

    最基本的分组收集器及其示例如下:

     

    2) 基本原理

    跟踪groupingBy的源代码如下:

    对最后一个重载的groupingBy方法返回的收集器,其收集元素的基本过程和伪代码为:

    在groupingBy函数中,默认的Map工厂方法为HashMap::new下游收集器为toList,它们都可以修改,实现更强大的功能。

     

    3) 分组收集

    通过修改Map工厂方法和下游收集器,在分组后可以进行一系列的自定义操作。

    下面java.util.stream.Collectors包中提供的一些常用下游收集器:

    下面是一些示例:

    注意:

    1. 存在更为通用的名为reducing的归约收集器,由于比较复杂且少用,暂不介绍。

     

    5) 收集前处理

    在分组后,直接交给下游收集器处理的一般为元素本身,可通过mapping方法为下游收集器组合一个前置Function,在下游收集前,对传入的元素进行映射转换等一系列处理。

     

    6) 收集后处理

    相应的,也可以通过collectingAndThen方法为下游收集器组合一个后置Function,在下游收集完成后,在分组内进行排序(sort)、过滤(filter)、限制返回元素(skip/limit)等一系列操作。

     

     

     

     

    7) 分区

    分组的一个特殊情况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:

    下面是一些简单示例:

     

    8) 多级分组

    groupingBy和partitioningBy都可以接受一个下游收集器,而下游收集器又可以是分组或分区。