一、沉默王二-集合容器
1.java集合框架
Java 集合框架可以分为两条大的支线:
①、Collection,主要由 List、Set、Queue 组成:
- List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;
- Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;
- Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。
②、Map,代表键值对的集合,典型代表就是 HashMap。
2.ArrayList
ArrayList 可以称得上是集合框架方面最常用的类了,可以和 HashMap 一较高下。
从名字就可以看得出来,ArrayList 实现了 List 接口,并且是基于数组实现的。
数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。
1)创建ArrayList
ArrayList<String> alist = new ArrayList<String>();
由于 ArrayList 实现了 List 接口,所以 alist 变量的类型可以是 List 类型;new 关键字声明后的尖括号中可以不再指定元素的类型,因为编译器可以通过前面尖括号中的类型进行智能推断。
此时会调用无参构造方法(见下面的代码)创建一个空的数组,常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为 {}。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2)添加元素
可以通过 add() 方法向 ArrayList 中添加一个元素。
alist.add("沉默王二");
我们来跟一下源码,看看 add 方法到底执行了哪些操作。跟的过程中,我们也可以偷师到 Java 源码的作者(大师级程序员)是如何优雅地写代码的。
堆栈过程图示:
add(element)
└── if (size == elementData.length) // 判断是否需要扩容
├── grow(minCapacity) // 扩容
│ └── newCapacity = oldCapacity + (oldCapacity >> 1) // 计算新的数组容量
│ └── Arrays.copyOf(elementData, newCapacity) // 创建新的数组
├── elementData[size++] = element; // 添加新元素
└── return true; // 添加成功
来具体看一下,先是 add() 方法的源码(已添加好详细地注释)
/**
* 将指定元素添加到 ArrayList 的末尾
* @param e 要添加的元素
* @return 添加成功返回 true
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保 ArrayList 能够容纳新的元素
elementData[size++] = e; // 在 ArrayList 的末尾添加指定元素
return true;
}
当容量不够时:if 条件为 true,进入 if 语句执行 grow() 方法,来看源码:
/**
* 扩容 ArrayList 的方法,确保能够容纳指定容量的元素
* @param minCapacity 指定容量的最小值
*/
private void grow(int minCapacity) {
// 检查是否会导致溢出,oldCapacity 为当前数组长度
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容至原来的1.5倍
if (newCapacity - minCapacity < 0) // 如果还是小于指定容量的最小值
newCapacity = minCapacity; // 直接扩容至指定容量的最小值
if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果超出了数组的最大长度
newCapacity = hugeCapacity(minCapacity); // 扩容至数组的最大长度
// 将当前数组复制到一个新数组中,长度为 newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList 在第一次执行 add 后会扩容为 10, ArrayList 第二次扩容在添加第 11 个元素时。
2)向指定位置添加元素
除了 add(E e) 方法,还可以通过 add(int index, E element) 方法把元素添加到 ArrayList 的指定位置:
alist.add(0, "沉默王三");
add(int index, E element) 方法的源码如下:
/**
* 在指定位置插入一个元素。
*
* @param index 要插入元素的位置
* @param element 要插入的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public void add(int index, E element) {
rangeCheckForAdd(index); // 检查索引是否越界
ensureCapacityInternal(size + 1); // 确保容量足够,如果需要扩容就扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 将 index 及其后面的元素向后移动一位
elementData[index] = element; // 将元素插入到指定位置
size++; // 元素个数加一
}
这是 arraycopy() 的语法:
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
在 ArrayList.add(int index, E element) 方法中,具体用法如下:
System.arraycopy(elementData, index, elementData, index + 1, size - index);
- elementData:表示要复制的源数组,即 ArrayList 中的元素数组。
- index:表示源数组中要复制的起始位置,即需要将 index 及其后面的元素向后移动一位。
- elementData:表示要复制到的目标数组,即 ArrayList 中的元素数组。
- index + 1:表示目标数组中复制的起始位置,即将 index 及其后面的元素向后移动一位后,应该插入到的位置。
- size - index:表示要复制的元素个数,即需要将 index 及其后面的元素向后移动一位,需要移动的元素个数为 size - index。
3)更新元素
可以使用 set() 方法来更改 ArrayList 中的元素,需要提供下标和新元素。
alist.set(0, "沉默王四");
假设原来 0 位置上的元素为“沉默王三”,现在可以将其更新为“沉默王四”。
来看一下 set() 方法的源码:
/**
* 用指定元素替换指定位置的元素。
*
* @param index 要替换的元素的索引
* @param element 要存储在指定位置的元素
* @return 先前在指定位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public E set(int index, E element) {
rangeCheck(index); // 检查索引是否越界
E oldValue = elementData(index); // 获取原来在指定位置上的元素
elementData[index] = element; // 将新元素替换到指定位置上
return oldValue; // 返回原来在指定位置上的元素
}
该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值。
4)删除元素
remove(int index) 方法用于删除指定下标位置上的元素,remove(Object o) 方法用于删除指定值的元素。
alist.remove(1);
alist.remove("沉默王四");
先来看 remove(int index) 方法的源码:
/**
* 删除指定位置的元素。
*
* @param index 要删除的元素的索引
* @return 先前在指定位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public E remove(int index) {
rangeCheck(index); // 检查索引是否越界
E oldValue = elementData(index); // 获取要删除的元素
int numMoved = size - index - 1; // 计算需要移动的元素个数
if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间
return oldValue; // 返回被删除的元素
}
需要注意的是,在 ArrayList 中,删除元素时,需要将删除位置后面的元素向前移动一位,以填补删除位置留下的空缺。如果需要移动元素,则需要使用 System.arraycopy 方法将删除位置后面的元素向前移动一位。最后,将数组末尾的元素置为 null,以便让垃圾回收机制回收该元素占用的空间。
再来看 remove(Object o) 方法的源码:
/**
* 删除列表中第一次出现的指定元素(如果存在)。
*
* @param o 要删除的元素
* @return 如果列表包含指定元素,则返回 true;否则返回 false
*/
public boolean remove(Object o) {
if (o == null) { // 如果要删除的元素是 null
for (int index = 0; index < size; index++) // 遍历列表
if (elementData[index] == null) { // 如果找到了 null 元素
fastRemove(index); // 调用 fastRemove 方法快速删除元素
return true; // 返回 true,表示成功删除元素
}
} else { // 如果要删除的元素不是 null
for (int index = 0; index < size; index++) // 遍历列表
if (o.equals(elementData[index])) { // 如果找到了要删除的元素
fastRemove(index); // 调用 fastRemove 方法快速删除元素
return true; // 返回 true,表示成功删除元素
}
}
return false; // 如果找不到要删除的元素,则返回 false
}
该方法通过遍历的方式找到要删除的元素,null 的时候使用 == 操作符判断,非 null 的时候使用 equals() 方法,然后调用 fastRemove() 方法。
5)查找元素
如果要正序查找一个元素,可以使用 indexOf() 方法;如果要倒序查找一个元素,可以使用 lastIndexOf() 方法。
alist.indexOf("沉默王二");
alist.lastIndexOf("沉默王二");
来看一下 indexOf() 方法的源码:
/**
* 返回指定元素在列表中第一次出现的位置。
* 如果列表不包含该元素,则返回 -1。
*
* @param o 要查找的元素
* @return 指定元素在列表中第一次出现的位置;如果列表不包含该元素,则返回 -1
*/
public int indexOf(Object o) {
if (o == null) { // 如果要查找的元素是 null
for (int i = 0; i < size; i++) // 遍历列表
if (elementData[i]==null) // 如果找到了 null 元素
return i; // 返回元素的索引
} else { // 如果要查找的元素不是 null
for (int i = 0; i < size; i++) // 遍历列表
if (o.equals(elementData[i])) // 如果找到了要查找的元素
return i; // 返回元素的索引
}
return -1; // 如果找不到要查找的元素,则返回 -1
}
如果元素为 null 的时候使用“==”操作符,否则使用 equals() 方法。
lastIndexOf() 方法和 indexOf() 方法类似,不过遍历的时候从最后开始。
contains() 方法可以判断 ArrayList 中是否包含某个元素,其内部就是通过 indexOf() 方法实现的:
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快。
Collections 类的 sort() 方法可以对 ArrayList 进行排序,该方法会按照字母顺序对 String 类型的列表进行排序。如果是自定义类型的列表,还可以指定 Comparator 进行排序。
排序后就可以使用二分查找法了:
int index = Collections.binarySearch(copy, "b");
3、LinkedList
- 对于第一个节点来说,prev 为 null;
- 对于最后一个节点来说,next 为 null;
- 其余的节点呢,prev 指向前一个,next 指向后一个。
1)增加元素
可以调用 add 方法添加元素:
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
add 方法内部其实调用的是 linkLast 方法:
/**
* 将指定的元素添加到列表的尾部。
*
* @param e 要添加到列表的元素
* @return 始终为 true(根据 Java 集合框架规范)
*/
public boolean add(E e) {
linkLast(e); // 在列表的尾部添加元素
return true; // 添加元素成功,返回 true
}
addFirst()方法将元素添加到第一位;addLast()方法将元素添加到末尾。
2)删除元素
remove():删除第一个节点remove(int):删除指定位置的节点remove(Object):删除指定元素的节点removeFirst():删除第一个节点removeLast():删除最后一个节点
remove(int) 内部其实调用的是 unlink 方法。
/**
* 删除指定位置上的元素。
*
* @param index 要删除的元素的索引
* @return 从列表中删除的元素
* @throws IndexOutOfBoundsException 如果索引越界(index < 0 || index >= size())
*/
public E remove(int index) {
checkElementIndex(index); // 检查索引是否越界
return unlink(node(index)); // 删除指定位置的节点,并返回节点的元素
}
unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。
/**
* 从链表中删除指定节点。
*
* @param x 要删除的节点
* @return 从链表中删除的节点的元素
*/
E unlink(Node<E> x) {
final E element = x.item; // 获取要删除节点的元素
final Node<E> next = x.next; // 获取要删除节点的下一个节点
final Node<E> prev = x.prev; // 获取要删除节点的上一个节点
if (prev == null) { // 如果要删除节点是第一个节点
first = next; // 将链表的头节点设置为要删除节点的下一个节点
} else {
prev.next = next; // 将要删除节点的上一个节点指向要删除节点的下一个节点
x.prev = null; // 将要删除节点的上一个节点设置为空
}
if (next == null) { // 如果要删除节点是最后一个节点
last = prev; // 将链表的尾节点设置为要删除节点的上一个节点
} else {
next.prev = prev; // 将要删除节点的下一个节点指向要删除节点的上一个节点
x.next = null; // 将要删除节点的下一个节点设置为空
}
x.item = null; // 将要删除节点的元素设置为空
size--; // 减少链表的元素个数
return element; // 返回被删除节点的元素
}
3)修改元素
可以调用 set() 方法来更新元素:
list.set(0, "沉默王五");
来看一下 set() 方法:
/**
* 将链表中指定位置的元素替换为指定元素,并返回原来的元素。
*
* @param index 要替换元素的位置(从 0 开始)
* @param element 要插入的元素
* @return 替换前的元素
* @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size())
*/
public E set(int index, E element) {
checkElementIndex(index); // 检查索引是否超出范围
Node<E> x = node(index); // 获取要替换的节点
E oldVal = x.item; // 获取要替换节点的元素
x.item = element; // 将要替换的节点的元素设置为指定元素
return oldVal; // 返回替换前的元素
}
来看一下node方法:
/**
* 获取链表中指定位置的节点。
*
* @param index 节点的位置(从 0 开始)
* @return 指定位置的节点
* @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size())
*/
Node<E> node(int index) {
if (index < (size >> 1)) { // 如果索引在链表的前半部分
Node<E> x = first;
for (int i = 0; i < index; i++) // 从头节点开始向后遍历链表,直到找到指定位置的节点
x = x.next;
return x; // 返回指定位置的节点
} else { // 如果索引在链表的后半部分
Node<E> x = last;
for (int i = size - 1; i > index; i--) // 从尾节点开始向前遍历链表,直到找到指定位置的节点
x = x.prev;
return x; // 返回指定位置的节点
}
}
4)查找元素
- indexOf(Object):查找某个元素所在的位置
- get(int):查找某个位置上的元素
来看一下 indexOf 方法的源码。
/**
* 返回链表中首次出现指定元素的位置,如果不存在该元素则返回 -1。
*
* @param o 要查找的元素
* @return 首次出现指定元素的位置,如果不存在该元素则返回 -1
*/
public int indexOf(Object o) {
int index = 0; // 初始化索引为 0
if (o == null) { // 如果要查找的元素为 null
for (Node<E> x = first; x != null; x = x.next) { // 从头节点开始向后遍历链表
if (x.item == null) // 如果找到了要查找的元素
return index; // 返回该元素的索引
index++; // 索引加 1
}
} else { // 如果要查找的元素不为 null
for (Node<E> x = first; x != null; x = x.next) { // 从头节点开始向后遍历链表
if (o.equals(x.item)) // 如果找到了要查找的元素
return index; // 返回该元素的索引
index++; // 索引加 1
}
}
return -1; // 如果没有找到要查找的元素,则返回 -1
}
get 方法的内核其实还是 node 方法,node 方法之前已经说明过了,这里略过。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
其实,查这个招式还可以演化为其他的一些,比如说:
getFirst()方法用于获取第一个元素;getLast()方法用于获取最后一个元素;poll()和pollFirst()方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的);pollLast()方法用于删除并返回最后一个元素;peekFirst()方法用于返回但不删除第一个元素。
二、小博哥-java设计模式
1、状态模式
状态模式描述的是一个行为下的多种状态变更,比如我们最常见的一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息),而这种登录与不登录就是我们通过改变状态,而让整个行为发生了变化。
优化后代码:
itstack-demo-design-19-02
└── src
└── main
└── java
└── org.itstack.demo.design
├── event
│ ├── CheckState.java
│ └── CloseState.java
│ └── DoingState.java
│ └── EditingState.java
│ └── OpenState.java
│ └── PassState.java
│ └── RefuseState.java
├── Result.java
├── State.java
└── StateHandler.java
- 以上是状态模式的整个工程结构模型,State是一个抽象类,定义了各种操作接口(
提审、审核、拒审等)。 - 右侧的不同颜色状态与我们场景模拟中的颜色保持一致,是各种状态流程流转的实现操作。这里的实现有一个关键点就是每一种状态到下一个状态,都分配到各个实现方法中控制,也就不需要
if语言进行判断了。 - 最后是
StateHandler对状态流程的统一处理,里面提供Map结构的各项服务接口调用,也就避免了使用if判断各项状态转变的流程。
2、策略模式
策略模式是一种行为模式,也是替代大量ifelse的利器。它所能帮你解决的是场景,一般是具有同类可替代的行为逻辑算法场景。比如:不同类型的交易方式(信用卡、支付宝、微信)、生成唯一ID策略(UUID、DB自增、DB+Redis、雪花算法、Leaf算法)等,都可以使用策略模式进行行为包装,供给外部使用。
itstack-demo-design-20-02
└── src
└── main
└── java
└── org.itstack.demo.design
├── event
│ └── MJCouponDiscount.java
│ └── NYGCouponDiscount.java
│ └── ZJCouponDiscount.java
│ └── ZKCouponDiscount.java
├── Context.java
└── ICouponDiscount.java
- 整体的结构模式并不复杂,主要体现的不同类型的优惠券在计算优惠券方式的不同计算策略。
- 这里包括一个接口类(
ICouponDiscount)以及四种优惠券类型的实现方式。 - 最后提供了策略模式的上下控制类处理,整体的策略服务。
3、模版模式
模板模式的核心设计思路是通过在,抽象类中定义抽象方法的执行顺序,并将抽象方法设定为只有子类实现,但不设计独立访问的方法。简单说也就是把你安排的明明白白的。
itstack-demo-design-21-00
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── group
│ │ ├── DangDangNetMall.java
│ │ ├── JDNetMall.java
│ │ └── TaoBaoNetMall.java
│ ├── HttpClient.java
│ └── NetMall.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
4、访问者模式
访问者要解决的核心事项是,在一个稳定的数据结构下,例如用户信息、雇员信息等,增加易变的业务访问逻辑。为了增强扩展性,将这两部分的业务解耦的一种设计模式。
itstack-demo-design-22-00
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── user
│ │ ├── impl
│ │ │ ├── Student.java
│ │ │ └── Teacher.java
│ │ └── User.java
│ ├── visitor
│ │ ├── impl
│ │ │ ├── Parent.java
│ │ │ └── Principal.java
│ │ └── Visitor.java
│ └── DataView.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
访问者模式的类结构相对其他设计模式来说比较复杂,但这样的设计模式在我看来更加烧气有魅力,它能开阔你对代码结构的新认知,用这样思维不断的建设出更好的代码架构。
关于这个案例的核心逻辑实现,有以下几点:
- 建立用户抽象类和抽象访问方法,再由不同的用户实现:老师和学生。
- 建立访问者接口,用于不同人员的访问操作:校长和家长。
- 最终是对数据的看板建设,用于实现不同视角的访问结果输出。
以上是视图展示了代码的核心结构,主要包括不同视角下的不同用户访问模型。
在这里有一个关键的点非常重要,也就是整套设计模式的核心组成部分;visitor.visit(this),这个方法在每一个用户实现类里,包括;Student、Teacher。在以下的实现中可以重点关注。
三、小博哥-spring专栏容器篇
1、Bean容器
Spring 包含并管理应用对象的配置和生命周期,在这个意义上它是一种用于承载对象的容器,你可以配置你的每个 Bean 对象是如何被创建的,这些 Bean 可以创建一个单独的实例或者每次需要时都生成一个新的实例,以及它们是如何相互关联构建和使用的。
如果一个 Bean 对象交给 Spring 容器管理,那么这个 Bean 对象就应该以类似零件的方式被拆解后存放到 Bean 的定义中,这样相当于一种把对象解耦的操作,可以由 Spring 更加容易的管理,就像处理循环依赖等操作。
当一个 Bean 对象被定义存放以后,再由 Spring 统一进行装配,这个过程包括 Bean 的初始化、属性填充等,最终我们就可以完整的使用一个 Bean 实例化后的对象了。
工程结构:
small-spring-step-01
└── src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── BeanDefinition.java
│ └── BeanFactory.java
└── test
└── java
└── cn.bugstack.springframework.test
├── bean
│ └── UserService.java
└── ApiTest.java
依赖关系:
Spring Bean 容器的整个实现内容非常简单,也仅仅是包括了一个简单的 BeanFactory 和 BeanDefinition,这里的类名称是与 Spring 源码中一致,只不过现在的类实现会相对来说更简化一些,在后续的实现过程中再不断的添加内容。
- BeanDefinition,用于定义 Bean 实例化信息,现在的实现是以一个 Object 存放对象
- BeanFactory,代表了 Bean 对象的工厂,可以存放 Bean 定义到 Map 中以及获取。
bean定义:
public class BeanDefinition { // 定义一个名为 BeanDefinition 的公共类
private Object bean; // 声明一个私有对象 bean
public BeanDefinition(Object bean) { // 构造函数,接受一个对象作为参数
this.bean = bean; // 将传入的参数赋值给类的私有成员 bean
}
public Object getBean() { // 公共方法,用于获取 bean 对象
return bean; // 返回 bean 的值
}
}
bean工厂:
public class BeanFactory { // 定义一个名为 BeanFactory 的公共类
// 声明一个私有的线程安全的映射,用于存储 BeanDefinition 对象
private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
public Object getBean(String name) { // 公共方法,根据名称获取对应的 bean 对象
return beanDefinitionMap.get(name).getBean(); // 从映射中获取指定名称的 BeanDefinition,并返回其 bean 对象
}
public void registerBeanDefinition(String name, BeanDefinition beanDefinition) { // 公共方法,注册一个新的 BeanDefinition
beanDefinitionMap.put(name, beanDefinition); // 将名称和对应的 BeanDefinition 添加到映射中
}
}
测试用例:
@Test
public void test_BeanFactory(){
// 1.初始化 BeanFactory
BeanFactory beanFactory = new BeanFactory();
// 2.注册 bean
BeanDefinition beanDefinition = new BeanDefinition(new UserService());
beanFactory.registerBeanDefinition("userService", beanDefinition);
// 3.获取 bean
UserService userService = (UserService) beanFactory.getBean("userService");
userService.queryUserInfo();
}